From f5f779b9ef74c50368aa5ea5b9d51f33010a40f5 Mon Sep 17 00:00:00 2001 From: Amazon GitHub Automation <54958958+amazon-auto@users.noreply.github.com> Date: Fri, 13 May 2022 12:45:07 -0400 Subject: [PATCH] Initial commit --- .github/ISSUE_TEMPLATE/bug_report.md | 42 + .github/ISSUE_TEMPLATE/feature_request.md | 17 + .github/PULL_REQUEST_TEMPLATE.md | 5 + .gitignore | 59 + CHANGELOG.md | 12 + CODE_OF_CONDUCT.md | 5 + CONTRIBUTING.md | 62 + LICENSE | 175 + LICENSE.txt | 175 + NOTICE | 1 + NOTICE.txt | 21 + README.md | 134 + deployment/build-open-source-dist.sh | 132 + deployment/build-s3-dist.sh | 408 + deployment/cdk-solution-helper/README.md | 152 + deployment/cdk-solution-helper/index.js | 130 + deployment/cdk-solution-helper/package.json | 10 + deployment/solution_config | 3 + .../aws-best-practices/accounts-config.yaml | 22 + .../backup-policies/org-backup-policies.json | 267 + ...h-ec2-instance-profile-detection-role.json | 16 + ...ec2-instance-profile-remediation-role.json | 14 + .../attach-ec2-instance-profile.zip | Bin 0 -> 987 bytes .../bucket-sse-enabled-remediation-role.json | 15 + ...ce-profile-permissions-detection-role.json | 13 + ...-profile-permissions-remediation-role.json | 13 + .../ec2-instance-profile-permissions.zip | Bin 0 -> 1292 bytes .../elb-logging-enabled-remediation-role.json | 14 + .../domain-list-1.txt | 2 + .../aws-best-practices/global-config.yaml | 72 + .../aws-best-practices/iam-config.yaml | 43 + .../iam-policies/boundary-policy.json | 10 + .../aws-best-practices/network-config.yaml | 534 + .../organization-config.yaml | 51 + .../aws-best-practices/security-config.yaml | 656 + .../deny-delete-vpc-flow-logs.json | 15 + .../guardrails-1.json | 91 + .../guardrails-2.json | 144 + .../service-control-policies/quarantine.json | 21 + .../attach-iam-instance-profile.yaml | 19 + .../ssm-documents/attach-iam-role-policy.yaml | 50 + .../ssm-documents/s3-encryption.yaml | 28 + .../ssm-documents/ssm-elb-enable-logging.yaml | 44 + .../tagging-policies/org-tag-policy.json | 15 + .../test-configuration/config.yaml | 22 + .../vpc-endpoint-policies/default.json | 10 + .../vpc-endpoint-policies/ec2.json | 10 + source/.eslintrc.json | 25 + source/.husky/pre-commit | 6 + source/.prettierrc.json | 7 + source/lerna.json | 6 + source/package.json | 81 + .../@aws-accelerator/accelerator/.npmignore | 2 + .../@aws-accelerator/accelerator/README.md | 1 + .../@aws-accelerator/accelerator/bin/app.ts | 510 + .../@aws-accelerator/accelerator/cdk.json | 6 + .../@aws-accelerator/accelerator/cdk.ts | 92 + .../@aws-accelerator/accelerator/index.ts | 32 + .../accelerator/jest.config.js | 6 + .../accelerator/lib/accelerator-stage.ts | 41 + .../accelerator/lib/accelerator.ts | 419 + .../accelerator/lib/config-repository.ts | 98 + .../accelerator/lib/detach-quarantine-scp.ts | 102 + .../lambdas/attach-quarantine-scp/index.ts | 114 + .../attach-quarantine-scp/package.json | 46 + .../attach-quarantine-scp/tsconfig.json | 8 + .../lambdas/detach-quarantine-scp/index.ts | 93 + .../detach-quarantine-scp/package.json | 46 + .../detach-quarantine-scp/tsconfig.json | 8 + .../lib/lambdas/load-config-table/index.ts | 321 + .../lambdas/load-config-table/package.json | 52 + .../lambdas/load-config-table/tsconfig.json | 9 + .../lib/lambdas/validate-environment/index.ts | 511 + .../lambdas/validate-environment/package.json | 50 + .../validate-environment/tsconfig.json | 8 + .../accelerator/lib/load-config-table.ts | 147 + .../accelerator/lib/logger.ts | 41 + .../accelerator/lib/pipeline.ts | 536 + .../lib/stacks/accelerator-stack.ts | 174 + .../accelerator/lib/stacks/accounts-stack.ts | 306 + .../lib/stacks/dependencies-stack.ts | 27 + .../accelerator/lib/stacks/finalize-stack.ts | 56 + .../accelerator/lib/stacks/key-stack.ts | 163 + .../accelerator/lib/stacks/logging-stack.ts | 356 + .../lib/stacks/network-associations-stack.ts | 624 + .../lib/stacks/network-prep-stack.ts | 465 + .../lib/stacks/network-vpc-dns-stack.ts | 308 + .../lib/stacks/network-vpc-endpoints-stack.ts | 825 ++ .../lib/stacks/network-vpc-stack.ts | 1283 ++ .../lib/stacks/operations-stack.ts | 276 + .../lib/stacks/organizations-stack.ts | 475 + .../accelerator/lib/stacks/pipeline-stack.ts | 145 + .../accelerator/lib/stacks/prepare-stack.ts | 415 + .../lib/stacks/security-audit-stack.ts | 430 + .../lib/stacks/security-resources-stack.ts | 753 ++ .../accelerator/lib/stacks/security-stack.ts | 194 + .../lib/stacks/tester-pipeline-stack.ts | 110 + .../accelerator/lib/tester-pipeline.ts | 225 + .../accelerator/lib/toolkit.ts | 220 + .../lib/validate-environment-config.ts | 134 + .../@aws-accelerator/accelerator/package.json | 77 + .../operations-stack.test.ts.snap | 273 + .../accelerator/test/accounts-stack.test.ts | 711 ++ .../backup-policies/org-backup-policies.json | 50 + .../configs/iam-policies/boundary-policy.json | 10 + .../accelerator/test/configs/metadataDocument | 0 .../deny-delete-vpc-flow-logs.json | 15 + .../service-control-policies/quarantine.json | 20 + .../tagging-policies/org-tag-policy.json | 15 + .../accelerator/test/configs/test-config.ts | 973 ++ .../vpc-endpoint-policies/default.json | 10 + .../accelerator/test/finalize-stack.test.ts | 121 + .../accelerator/test/logging-stack.test.ts | 428 + .../test/network-associations-stack.test.ts | 355 + .../test/network-prep-stack.test.ts | 371 + .../test/network-vpc-dns-stack.test.ts | 258 + .../test/network-vpc-endpoints-stack.test.ts | 257 + .../test/network-vpc-stack.test.ts | 228 + .../accelerator/test/operations-stack.test.ts | 436 + .../test/organizations-stack.test.ts | 880 ++ .../accelerator/test/pipeline-stack.test.ts | 1614 +++ .../test/security-audit-stack.test.ts | 1477 +++ .../accelerator/test/security-stack.test.ts | 590 + .../test/tester-pipeline-stack.test.ts | 1155 ++ .../accelerator/tsconfig.json | 8 + .../packages/@aws-accelerator/config/index.ts | 20 + .../config/lib/accounts-config.ts | 371 + .../config/lib/common-types/index.ts | 16 + .../config/lib/common-types/parse.ts | 26 + .../config/lib/common-types/reporter.ts | 61 + .../config/lib/common-types/types.ts | 328 + .../config/lib/global-config.ts | 639 + .../@aws-accelerator/config/lib/iam-config.ts | 522 + .../config/lib/network-config.ts | 2770 ++++ .../config/lib/organization-config.ts | 514 + .../config/lib/security-config.ts | 1404 +++ .../@aws-accelerator/config/package.json | 49 + .../config/test/organization.test.ts | 24 + .../@aws-accelerator/config/tsconfig.json | 8 + .../@aws-accelerator/constructs/.gitignore | 9 + .../@aws-accelerator/constructs/.npmignore | 6 + .../@aws-accelerator/constructs/README.md | 1 + .../@aws-accelerator/constructs/index.ts | 70 + .../constructs/jest.config.js | 6 + .../lib/aws-budgets/budget-definition.ts | 100 + .../create-accounts-status/index.ts | 282 + .../create-accounts-status/package.json | 46 + .../create-accounts-status/tsconfig.json | 8 + .../lib/aws-controltower/create-accounts.ts | 177 + .../aws-controltower/create-accounts/index.ts | 39 + .../create-accounts/package.json | 44 + .../create-accounts/tsconfig.json | 8 + .../cross-region-report-definition/index.ts | 102 + .../package.json | 44 + .../tsconfig.json | 8 + .../lib/aws-cur/report-definition.ts | 240 + .../lib/aws-ec2/delete-default-vpc.ts | 94 + .../lib/aws-ec2/delete-default-vpc/index.ts | 239 + .../aws-ec2/delete-default-vpc/package.json | 45 + .../aws-ec2/delete-default-vpc/tsconfig.json | 8 + .../constructs/lib/aws-ec2/dhcp-options.ts | 92 + .../aws-ec2/ebs-default-encryption/index.ts | 54 + .../ebs-default-encryption/package.json | 45 + .../ebs-default-encryption/tsconfig.json | 8 + .../constructs/lib/aws-ec2/ebs-encryption.ts | 99 + .../get-transit-gateway-attachment/index.ts | 88 + .../package.json | 46 + .../tsconfig.json | 8 + .../constructs/lib/aws-ec2/prefix-list.ts | 77 + .../constructs/lib/aws-ec2/route-table.ts | 101 + .../aws-ec2/transit-gateway-route-table.ts | 53 + .../aws-ec2/transit-gateway-static-route.ts | 54 + .../constructs/lib/aws-ec2/transit-gateway.ts | 320 + .../constructs/lib/aws-ec2/vpc-endpoint.ts | 99 + .../constructs/lib/aws-ec2/vpc-peering.ts | 94 + .../constructs/lib/aws-ec2/vpc.ts | 545 + .../lib/aws-guardduty/create-members/index.ts | 114 + .../aws-guardduty/create-members/package.json | 45 + .../create-members/tsconfig.json | 8 + .../create-publishing-destination/index.ts | 111 + .../package.json | 44 + .../tsconfig.json | 8 + .../index.ts | 110 + .../package.json | 45 + .../tsconfig.json | 8 + .../guardduty-detector-config.ts | 110 + .../lib/aws-guardduty/guardduty-members.ts | 115 + .../guardduty-organization-admin-account.ts | 120 + .../guardduty-publishing-destination.ts | 100 + .../update-detector-config/index.ts | 118 + .../update-detector-config/package.json | 45 + .../update-detector-config/tsconfig.json | 8 + .../constructs/lib/aws-iam/password-policy.ts | 98 + .../update-account-password-policy/index.ts | 62 + .../package.json | 43 + .../tsconfig.json | 8 + .../constructs/lib/aws-kms/key-lookup.ts | 73 + .../lib/aws-macie/create-member/index.ts | 162 + .../lib/aws-macie/create-member/package.json | 45 + .../lib/aws-macie/create-member/tsconfig.json | 8 + .../lib/aws-macie/enable-macie/index.ts | 115 + .../lib/aws-macie/enable-macie/package.json | 45 + .../lib/aws-macie/enable-macie/tsconfig.json | 8 + .../index.ts | 154 + .../package.json | 46 + .../tsconfig.json | 8 + .../macie-export-config-classification.ts | 98 + .../constructs/lib/aws-macie/macie-members.ts | 109 + .../macie-organization-admin-account.ts | 136 + .../constructs/lib/aws-macie/macie-session.ts | 109 + .../put-export-config-classification/index.ts | 89 + .../package.json | 45 + .../tsconfig.json | 8 + .../lib/aws-networkfirewall/firewall.ts | 156 + .../get-network-firewall-endpoint.ts | 97 + .../get-network-firewall-endpoint/index.ts | 69 + .../package.json | 45 + .../tsconfig.json | 8 + .../lib/aws-networkfirewall/policy.ts | 160 + .../lib/aws-networkfirewall/rule-group.ts | 216 + .../lib/aws-organizations/account.ts | 105 + .../aws-organizations/attach-policy/index.ts | 110 + .../attach-policy/package.json | 46 + .../attach-policy/tsconfig.json | 8 + .../create-accounts-status/index.ts | 306 + .../create-accounts-status/package.json | 46 + .../create-accounts-status/tsconfig.json | 8 + .../lib/aws-organizations/create-accounts.ts | 143 + .../create-accounts/index.ts | 39 + .../create-accounts/package.json | 44 + .../create-accounts/tsconfig.json | 8 + .../create-organizational-units/index.ts | 255 + .../create-organizational-units/package.json | 44 + .../create-organizational-units/tsconfig.json | 8 + .../aws-organizations/create-policy/index.ts | 156 + .../create-policy/package.json | 44 + .../create-policy/tsconfig.json | 8 + .../describe-organization/index.ts | 56 + .../describe-organization/package.json | 43 + .../describe-organization/tsconfig.json | 8 + .../enable-aws-service-access.ts | 85 + .../enable-aws-service-access/index.ts | 62 + .../enable-aws-service-access/package.json | 43 + .../enable-aws-service-access/tsconfig.json | 8 + .../aws-organizations/enable-policy-type.ts | 115 + .../enable-policy-type/index.ts | 88 + .../enable-policy-type/package.json | 43 + .../enable-policy-type/tsconfig.json | 8 + .../invite-account-to-organization/index.ts | 117 + .../package.json | 43 + .../tsconfig.json | 8 + .../lib/aws-organizations/organization.ts | 50 + .../aws-organizations/organizational-units.ts | 105 + .../aws-organizations/policy-attachment.ts | 103 + .../lib/aws-organizations/policy.ts | 175 + .../register-delegated-administrator.ts | 88 + .../register-delegated-administrator/index.ts | 83 + .../package.json | 43 + .../tsconfig.json | 8 + .../enable-sharing-with-aws-organization.ts | 84 + .../index.ts | 48 + .../package.json | 43 + .../tsconfig.json | 8 + .../aws-ram/get-resource-share-item/index.ts | 81 + .../get-resource-share-item/package.json | 43 + .../get-resource-share-item/tsconfig.json | 8 + .../lib/aws-ram/get-resource-share/index.ts | 66 + .../aws-ram/get-resource-share/package.json | 43 + .../aws-ram/get-resource-share/tsconfig.json | 8 + .../constructs/lib/aws-ram/resource-share.ts | 232 + .../endpoint-addresses.ts | 84 + .../firewall-domain-list.ts | 150 + .../firewall-rule-group.ts | 112 + .../get-domain-lists/index.ts | 72 + .../get-domain-lists/package.json | 44 + .../get-domain-lists/tsconfig.json | 8 + .../get-endpoint-addresses/index.ts | 81 + .../get-endpoint-addresses/package.json | 44 + .../get-endpoint-addresses/tsconfig.json | 8 + .../query-logging-config.ts | 121 + .../resolver-endpoint.ts | 88 + .../aws-route-53-resolver/resolver-rule.ts | 145 + .../aws-route-53/associate-hosted-zones.ts | 96 + .../associate-hosted-zones/index.ts | 164 + .../associate-hosted-zones/package.json | 43 + .../associate-hosted-zones/tsconfig.json | 8 + .../lib/aws-route-53/hosted-zone.ts | 63 + .../constructs/lib/aws-route-53/record-set.ts | 62 + .../constructs/lib/aws-s3/bucket.ts | 312 + .../lib/aws-s3/central-logs-bucket.ts | 213 + .../lib/aws-s3/public-access-block.ts | 91 + .../aws-s3/put-public-access-block/index.ts | 67 + .../put-public-access-block/package.json | 43 + .../put-public-access-block/tsconfig.json | 8 + .../batch-enable-standards/index.ts | 334 + .../batch-enable-standards/package.json | 43 + .../batch-enable-standards/tsconfig.json | 8 + .../aws-securityhub/create-members/index.ts | 114 + .../create-members/package.json | 43 + .../create-members/tsconfig.json | 8 + .../index.ts | 171 + .../package.json | 43 + .../tsconfig.json | 8 + .../aws-securityhub/securityhub-members.ts | 102 + .../securityhub-organization-admin-account.ts | 134 + .../aws-securityhub/securityhub-standards.ts | 96 + .../aws-servicecatalog/get-portfolio-id.ts | 84 + .../get-portfolio-id/index.ts | 68 + .../get-portfolio-id/package.json | 46 + .../get-portfolio-id/tsconfig.json | 8 + .../constructs/lib/aws-ssm/document.ts | 99 + .../lib/aws-ssm/get-param-value/index.ts | 79 + .../lib/aws-ssm/get-param-value/package.json | 43 + .../lib/aws-ssm/get-param-value/tsconfig.json | 8 + .../lib/aws-ssm/put-param-value/index.ts | 89 + .../lib/aws-ssm/put-param-value/package.json | 43 + .../lib/aws-ssm/put-param-value/tsconfig.json | 8 + .../lib/aws-ssm/session-manager-settings.ts | 275 + .../aws-ssm/session-manager-settings/index.ts | 100 + .../session-manager-settings/package.json | 43 + .../session-manager-settings/tsconfig.json | 8 + .../lib/aws-ssm/share-document/index.ts | 101 + .../lib/aws-ssm/share-document/package.json | 44 + .../lib/aws-ssm/share-document/tsconfig.json | 8 + .../lib/aws-ssm/ssm-parameter-lookup.ts | 108 + .../constructs/lib/aws-ssm/ssm-parameter.ts | 159 + .../@aws-accelerator/constructs/package.json | 49 + .../test/aws-cur/report-definition.test.ts | 349 + .../__snapshots__/dhcp-options.test.ts.snap | 30 + .../__snapshots__/prefix-list.test.ts.snap | 27 + .../__snapshots__/route-table.test.ts.snap | 63 + .../transit-gateway-route-table.test.ts.snap | 24 + .../transit-gateway.test.ts.snap | 15 + .../__snapshots__/vpc-endpoint.test.ts.snap | 65 + .../aws-ec2/__snapshots__/vpc.test.ts.snap | 58 + .../test/aws-ec2/delete-default-vpc.test.ts | 160 + .../test/aws-ec2/dhcp-options.test.ts | 78 + .../test/aws-ec2/prefix-list.test.ts | 68 + .../test/aws-ec2/route-table.test.ts | 164 + .../transit-gateway-route-table.test.ts | 70 + .../test/aws-ec2/transit-gateway.test.ts | 60 + .../test/aws-ec2/vpc-endpoint.test.ts | 154 + .../test/aws-ec2/vpc-peering.test.ts | 71 + .../constructs/test/aws-ec2/vpc.test.ts | 156 + .../guardduty-detector-config.test.ts | 161 + .../aws-guardduty/guardduty-members.test.ts | 178 + ...ardduty-organization-admin-account.test.ts | 190 + .../guardduty-publishing-destination.test.ts | 188 + .../test/aws-iam/password-policy.test.ts | 164 + .../test/aws-macie/macie-members.test.ts | 174 + .../macie-organization-admin-account.test.ts | 202 + .../test/aws-macie/macie-session.test.ts | 170 + .../test/aws-networkfirewall/firewall.test.ts | 75 + .../get-network-firewall-endpoint.test.ts | 180 + .../test/aws-networkfirewall/policy.test.ts | 81 + .../aws-networkfirewall/rule-group.test.ts | 122 + .../test/aws-organizations/account.test.ts | 173 + .../enable-aws-service-access.test.ts | 148 + .../enable-policy-type.test.ts | 173 + .../aws-organizations/organization.test.ts | 143 + .../organizational-units.test.ts | 177 + .../policy-attachment.test.ts | 156 + .../test/aws-organizations/policy.test.ts | 196 + .../register-delegated-administrator.test.ts | 161 + .../__snapshots__/resource-share.test.ts.snap | 62 + ...able-sharing-with-aws-organization.test.ts | 152 + .../test/aws-ram/resource-share.test.ts | 113 + .../endpoint-addresses.test.ts | 146 + .../firewall-domain-list.test.ts | 194 + .../firewall-rule-group.test.ts | 107 + .../query-logging-config.test.ts | 135 + .../resolver-endpoint.test.ts | 61 + .../resolver-rule.test.ts | 104 + .../__snapshots__/hosted-zone.test.ts.snap | 22 + .../__snapshots__/record-set.test.ts.snap | 140 + .../test/aws-route-53/hosted-zone.test.ts | 72 + .../test/aws-route-53/record-set.test.ts | 271 + .../central-logs-buckets.test.ts.snap | 648 + .../test/aws-s3/central-logs-buckets.test.ts | 535 + .../test/aws-s3/public-access-block.test.ts | 158 + .../securityhub-members.test.ts | 168 + ...rityhub-organization-admin-account.test.ts | 222 + .../securityhub-standards.test.ts | 184 + .../aws-ssm/session-manager-settings.test.ts | 760 ++ .../test/aws-ssm/ssm-parameter-lookup.test.ts | 94 + .../@aws-accelerator/constructs/tsconfig.json | 6 + .../@aws-accelerator/installer/.npmignore | 6 + .../@aws-accelerator/installer/README.md | 1 + .../installer/bin/installer.ts | 43 + .../@aws-accelerator/installer/cdk.json | 5 + .../@aws-accelerator/installer/index.ts | 14 + .../@aws-accelerator/installer/jest.config.js | 6 + .../installer/lib/installer-stack.ts | 743 ++ .../installer/lib/solutions-helper.ts | 189 + .../@aws-accelerator/installer/package.json | 48 + .../test/__snapshots__/installer.test.ts.snap | 7871 ++++++++++++ .../installer/test/installer.test.ts | 2439 ++++ .../@aws-accelerator/installer/tsconfig.json | 8 + .../@aws-accelerator/tester/.npmignore | 6 + .../@aws-accelerator/tester/README.md | 1 + .../@aws-accelerator/tester/bin/app.ts | 79 + .../packages/@aws-accelerator/tester/cdk.json | 7 + .../packages/@aws-accelerator/tester/index.ts | 14 + .../@aws-accelerator/tester/jest.config.js | 6 + .../@aws-accelerator/tester/lambdas/index.ts | 118 + .../tester/lambdas/package.json | 46 + .../validate-transit-gateway.ts | 277 + .../tester/lambdas/tsconfig.json | 11 + .../tester/lib/tester-stack.ts | 149 + .../@aws-accelerator/tester/package.json | 55 + .../tester/test/configs/config.yaml | 22 + ...rnal-pipeline-account-tester-stack.test.ts | 331 + .../tester/test/tester-stack.test.ts | 329 + .../@aws-accelerator/tester/tsconfig.json | 10 + .../packages/@aws-accelerator/tools/index.ts | 15 + .../tools/lib/classes/accelerator-tool.ts | 1557 +++ .../@aws-accelerator/tools/package.json | 58 + .../tools/test/uninstaller.test.ts | 22 + .../@aws-accelerator/tools/tsconfig.json | 8 + .../@aws-accelerator/tools/uninstaller.ts | 80 + .../packages/@aws-accelerator/utils/README.md | 1 + .../packages/@aws-accelerator/utils/index.ts | 1 + .../@aws-accelerator/utils/lib/throttle.ts | 64 + .../@aws-accelerator/utils/package.json | 45 + .../utils/test/throttle.test.ts | 25 + .../@aws-accelerator/utils/tsconfig.json | 8 + .../cdk-extensions/.gitignore | 8 + .../cdk-extensions/.npmignore | 6 + .../cdk-extensions/README.md | 1 + .../cdk-extensions/index.ts | 15 + .../cdk-extensions/jest.config.js | 6 + .../cdk-extensions/lib/repository.ts | 64 + .../cdk-extensions/lib/trail.ts | 28 + .../cdk-extensions/package.json | 48 + .../repository-snapshot.test.ts.snap | 21 + .../__snapshots__/repository.test.ts.snap | 21 + .../test/repository-fine-grained.test.ts | 36 + .../test/repository-snapshot.test.ts | 27 + .../cdk-extensions/test/repository.test.ts | 62 + .../cdk-extensions/test/test-config.ts | 29 + .../cdk-extensions/tsconfig.json | 8 + .../cdk-plugin-assume-role/.gitignore | 1 + .../cdk-plugin-assume-role/index.ts | 15 + .../lib/assume-role-plugin.ts | 51 + .../lib/assume-role-provider-source.ts | 90 + .../cdk-plugin-assume-role/lib/backoff.ts | 44 + .../cdk-plugin-assume-role/package.json | 45 + .../cdk-plugin-assume-role/tsconfig.json | 8 + source/run-all-tests.sh | 157 + source/tsconfig.json | 24 + source/yarn.lock | 10419 ++++++++++++++++ 451 files changed, 82169 insertions(+) create mode 100755 .github/ISSUE_TEMPLATE/bug_report.md create mode 100755 .github/ISSUE_TEMPLATE/feature_request.md create mode 100755 .github/PULL_REQUEST_TEMPLATE.md create mode 100755 .gitignore create mode 100755 CHANGELOG.md create mode 100755 CODE_OF_CONDUCT.md create mode 100755 CONTRIBUTING.md create mode 100644 LICENSE create mode 100755 LICENSE.txt create mode 100644 NOTICE create mode 100755 NOTICE.txt create mode 100755 README.md create mode 100755 deployment/build-open-source-dist.sh create mode 100755 deployment/build-s3-dist.sh create mode 100755 deployment/cdk-solution-helper/README.md create mode 100755 deployment/cdk-solution-helper/index.js create mode 100755 deployment/cdk-solution-helper/package.json create mode 100644 deployment/solution_config create mode 100644 reference/sample-configurations/aws-best-practices/accounts-config.yaml create mode 100644 reference/sample-configurations/aws-best-practices/backup-policies/org-backup-policies.json create mode 100644 reference/sample-configurations/aws-best-practices/custom-config-rules/attach-ec2-instance-profile-detection-role.json create mode 100644 reference/sample-configurations/aws-best-practices/custom-config-rules/attach-ec2-instance-profile-remediation-role.json create mode 100644 reference/sample-configurations/aws-best-practices/custom-config-rules/attach-ec2-instance-profile.zip create mode 100644 reference/sample-configurations/aws-best-practices/custom-config-rules/bucket-sse-enabled-remediation-role.json create mode 100644 reference/sample-configurations/aws-best-practices/custom-config-rules/ec2-instance-profile-permissions-detection-role.json create mode 100644 reference/sample-configurations/aws-best-practices/custom-config-rules/ec2-instance-profile-permissions-remediation-role.json create mode 100644 reference/sample-configurations/aws-best-practices/custom-config-rules/ec2-instance-profile-permissions.zip create mode 100644 reference/sample-configurations/aws-best-practices/custom-config-rules/elb-logging-enabled-remediation-role.json create mode 100644 reference/sample-configurations/aws-best-practices/dns-firewall-domain-lists/domain-list-1.txt create mode 100644 reference/sample-configurations/aws-best-practices/global-config.yaml create mode 100644 reference/sample-configurations/aws-best-practices/iam-config.yaml create mode 100644 reference/sample-configurations/aws-best-practices/iam-policies/boundary-policy.json create mode 100644 reference/sample-configurations/aws-best-practices/network-config.yaml create mode 100644 reference/sample-configurations/aws-best-practices/organization-config.yaml create mode 100644 reference/sample-configurations/aws-best-practices/security-config.yaml create mode 100644 reference/sample-configurations/aws-best-practices/service-control-policies/deny-delete-vpc-flow-logs.json create mode 100644 reference/sample-configurations/aws-best-practices/service-control-policies/guardrails-1.json create mode 100644 reference/sample-configurations/aws-best-practices/service-control-policies/guardrails-2.json create mode 100644 reference/sample-configurations/aws-best-practices/service-control-policies/quarantine.json create mode 100644 reference/sample-configurations/aws-best-practices/ssm-documents/attach-iam-instance-profile.yaml create mode 100644 reference/sample-configurations/aws-best-practices/ssm-documents/attach-iam-role-policy.yaml create mode 100644 reference/sample-configurations/aws-best-practices/ssm-documents/s3-encryption.yaml create mode 100644 reference/sample-configurations/aws-best-practices/ssm-documents/ssm-elb-enable-logging.yaml create mode 100644 reference/sample-configurations/aws-best-practices/tagging-policies/org-tag-policy.json create mode 100644 reference/sample-configurations/aws-best-practices/test-configuration/config.yaml create mode 100644 reference/sample-configurations/aws-best-practices/vpc-endpoint-policies/default.json create mode 100644 reference/sample-configurations/aws-best-practices/vpc-endpoint-policies/ec2.json create mode 100644 source/.eslintrc.json create mode 100755 source/.husky/pre-commit create mode 100644 source/.prettierrc.json create mode 100644 source/lerna.json create mode 100644 source/package.json create mode 100644 source/packages/@aws-accelerator/accelerator/.npmignore create mode 100644 source/packages/@aws-accelerator/accelerator/README.md create mode 100644 source/packages/@aws-accelerator/accelerator/bin/app.ts create mode 100644 source/packages/@aws-accelerator/accelerator/cdk.json create mode 100644 source/packages/@aws-accelerator/accelerator/cdk.ts create mode 100644 source/packages/@aws-accelerator/accelerator/index.ts create mode 100644 source/packages/@aws-accelerator/accelerator/jest.config.js create mode 100644 source/packages/@aws-accelerator/accelerator/lib/accelerator-stage.ts create mode 100644 source/packages/@aws-accelerator/accelerator/lib/accelerator.ts create mode 100644 source/packages/@aws-accelerator/accelerator/lib/config-repository.ts create mode 100644 source/packages/@aws-accelerator/accelerator/lib/detach-quarantine-scp.ts create mode 100644 source/packages/@aws-accelerator/accelerator/lib/lambdas/attach-quarantine-scp/index.ts create mode 100644 source/packages/@aws-accelerator/accelerator/lib/lambdas/attach-quarantine-scp/package.json create mode 100644 source/packages/@aws-accelerator/accelerator/lib/lambdas/attach-quarantine-scp/tsconfig.json create mode 100644 source/packages/@aws-accelerator/accelerator/lib/lambdas/detach-quarantine-scp/index.ts create mode 100644 source/packages/@aws-accelerator/accelerator/lib/lambdas/detach-quarantine-scp/package.json create mode 100644 source/packages/@aws-accelerator/accelerator/lib/lambdas/detach-quarantine-scp/tsconfig.json create mode 100644 source/packages/@aws-accelerator/accelerator/lib/lambdas/load-config-table/index.ts create mode 100644 source/packages/@aws-accelerator/accelerator/lib/lambdas/load-config-table/package.json create mode 100644 source/packages/@aws-accelerator/accelerator/lib/lambdas/load-config-table/tsconfig.json create mode 100644 source/packages/@aws-accelerator/accelerator/lib/lambdas/validate-environment/index.ts create mode 100644 source/packages/@aws-accelerator/accelerator/lib/lambdas/validate-environment/package.json create mode 100644 source/packages/@aws-accelerator/accelerator/lib/lambdas/validate-environment/tsconfig.json create mode 100644 source/packages/@aws-accelerator/accelerator/lib/load-config-table.ts create mode 100644 source/packages/@aws-accelerator/accelerator/lib/logger.ts create mode 100644 source/packages/@aws-accelerator/accelerator/lib/pipeline.ts create mode 100644 source/packages/@aws-accelerator/accelerator/lib/stacks/accelerator-stack.ts create mode 100644 source/packages/@aws-accelerator/accelerator/lib/stacks/accounts-stack.ts create mode 100644 source/packages/@aws-accelerator/accelerator/lib/stacks/dependencies-stack.ts create mode 100644 source/packages/@aws-accelerator/accelerator/lib/stacks/finalize-stack.ts create mode 100644 source/packages/@aws-accelerator/accelerator/lib/stacks/key-stack.ts create mode 100644 source/packages/@aws-accelerator/accelerator/lib/stacks/logging-stack.ts create mode 100644 source/packages/@aws-accelerator/accelerator/lib/stacks/network-associations-stack.ts create mode 100644 source/packages/@aws-accelerator/accelerator/lib/stacks/network-prep-stack.ts create mode 100644 source/packages/@aws-accelerator/accelerator/lib/stacks/network-vpc-dns-stack.ts create mode 100644 source/packages/@aws-accelerator/accelerator/lib/stacks/network-vpc-endpoints-stack.ts create mode 100644 source/packages/@aws-accelerator/accelerator/lib/stacks/network-vpc-stack.ts create mode 100644 source/packages/@aws-accelerator/accelerator/lib/stacks/operations-stack.ts create mode 100644 source/packages/@aws-accelerator/accelerator/lib/stacks/organizations-stack.ts create mode 100644 source/packages/@aws-accelerator/accelerator/lib/stacks/pipeline-stack.ts create mode 100644 source/packages/@aws-accelerator/accelerator/lib/stacks/prepare-stack.ts create mode 100644 source/packages/@aws-accelerator/accelerator/lib/stacks/security-audit-stack.ts create mode 100644 source/packages/@aws-accelerator/accelerator/lib/stacks/security-resources-stack.ts create mode 100644 source/packages/@aws-accelerator/accelerator/lib/stacks/security-stack.ts create mode 100644 source/packages/@aws-accelerator/accelerator/lib/stacks/tester-pipeline-stack.ts create mode 100644 source/packages/@aws-accelerator/accelerator/lib/tester-pipeline.ts create mode 100644 source/packages/@aws-accelerator/accelerator/lib/toolkit.ts create mode 100644 source/packages/@aws-accelerator/accelerator/lib/validate-environment-config.ts create mode 100644 source/packages/@aws-accelerator/accelerator/package.json create mode 100644 source/packages/@aws-accelerator/accelerator/test/__snapshots__/operations-stack.test.ts.snap create mode 100644 source/packages/@aws-accelerator/accelerator/test/accounts-stack.test.ts create mode 100644 source/packages/@aws-accelerator/accelerator/test/configs/backup-policies/org-backup-policies.json create mode 100644 source/packages/@aws-accelerator/accelerator/test/configs/iam-policies/boundary-policy.json create mode 100644 source/packages/@aws-accelerator/accelerator/test/configs/metadataDocument create mode 100644 source/packages/@aws-accelerator/accelerator/test/configs/service-control-policies/deny-delete-vpc-flow-logs.json create mode 100644 source/packages/@aws-accelerator/accelerator/test/configs/service-control-policies/quarantine.json create mode 100644 source/packages/@aws-accelerator/accelerator/test/configs/tagging-policies/org-tag-policy.json create mode 100644 source/packages/@aws-accelerator/accelerator/test/configs/test-config.ts create mode 100644 source/packages/@aws-accelerator/accelerator/test/configs/vpc-endpoint-policies/default.json create mode 100644 source/packages/@aws-accelerator/accelerator/test/finalize-stack.test.ts create mode 100644 source/packages/@aws-accelerator/accelerator/test/logging-stack.test.ts create mode 100644 source/packages/@aws-accelerator/accelerator/test/network-associations-stack.test.ts create mode 100644 source/packages/@aws-accelerator/accelerator/test/network-prep-stack.test.ts create mode 100644 source/packages/@aws-accelerator/accelerator/test/network-vpc-dns-stack.test.ts create mode 100644 source/packages/@aws-accelerator/accelerator/test/network-vpc-endpoints-stack.test.ts create mode 100644 source/packages/@aws-accelerator/accelerator/test/network-vpc-stack.test.ts create mode 100644 source/packages/@aws-accelerator/accelerator/test/operations-stack.test.ts create mode 100644 source/packages/@aws-accelerator/accelerator/test/organizations-stack.test.ts create mode 100644 source/packages/@aws-accelerator/accelerator/test/pipeline-stack.test.ts create mode 100644 source/packages/@aws-accelerator/accelerator/test/security-audit-stack.test.ts create mode 100644 source/packages/@aws-accelerator/accelerator/test/security-stack.test.ts create mode 100644 source/packages/@aws-accelerator/accelerator/test/tester-pipeline-stack.test.ts create mode 100644 source/packages/@aws-accelerator/accelerator/tsconfig.json create mode 100644 source/packages/@aws-accelerator/config/index.ts create mode 100644 source/packages/@aws-accelerator/config/lib/accounts-config.ts create mode 100644 source/packages/@aws-accelerator/config/lib/common-types/index.ts create mode 100644 source/packages/@aws-accelerator/config/lib/common-types/parse.ts create mode 100644 source/packages/@aws-accelerator/config/lib/common-types/reporter.ts create mode 100644 source/packages/@aws-accelerator/config/lib/common-types/types.ts create mode 100644 source/packages/@aws-accelerator/config/lib/global-config.ts create mode 100644 source/packages/@aws-accelerator/config/lib/iam-config.ts create mode 100644 source/packages/@aws-accelerator/config/lib/network-config.ts create mode 100644 source/packages/@aws-accelerator/config/lib/organization-config.ts create mode 100644 source/packages/@aws-accelerator/config/lib/security-config.ts create mode 100644 source/packages/@aws-accelerator/config/package.json create mode 100644 source/packages/@aws-accelerator/config/test/organization.test.ts create mode 100644 source/packages/@aws-accelerator/config/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/.gitignore create mode 100644 source/packages/@aws-accelerator/constructs/.npmignore create mode 100644 source/packages/@aws-accelerator/constructs/README.md create mode 100644 source/packages/@aws-accelerator/constructs/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/jest.config.js create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-budgets/budget-definition.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-controltower/create-accounts-status/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-controltower/create-accounts-status/package.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-controltower/create-accounts-status/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-controltower/create-accounts.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-controltower/create-accounts/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-controltower/create-accounts/package.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-controltower/create-accounts/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-cur/cross-region-report-definition/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-cur/cross-region-report-definition/package.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-cur/cross-region-report-definition/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-cur/report-definition.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ec2/delete-default-vpc.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ec2/delete-default-vpc/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ec2/delete-default-vpc/package.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ec2/delete-default-vpc/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ec2/dhcp-options.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ec2/ebs-default-encryption/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ec2/ebs-default-encryption/package.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ec2/ebs-default-encryption/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ec2/ebs-encryption.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ec2/get-transit-gateway-attachment/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ec2/get-transit-gateway-attachment/package.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ec2/get-transit-gateway-attachment/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ec2/prefix-list.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ec2/route-table.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ec2/transit-gateway-route-table.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ec2/transit-gateway-static-route.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ec2/transit-gateway.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ec2/vpc-endpoint.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ec2/vpc-peering.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ec2/vpc.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-guardduty/create-members/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-guardduty/create-members/package.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-guardduty/create-members/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-guardduty/create-publishing-destination/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-guardduty/create-publishing-destination/package.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-guardduty/create-publishing-destination/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-guardduty/enable-organization-admin-account/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-guardduty/enable-organization-admin-account/package.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-guardduty/enable-organization-admin-account/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-guardduty/guardduty-detector-config.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-guardduty/guardduty-members.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-guardduty/guardduty-organization-admin-account.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-guardduty/guardduty-publishing-destination.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-guardduty/update-detector-config/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-guardduty/update-detector-config/package.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-guardduty/update-detector-config/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-iam/password-policy.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-iam/update-account-password-policy/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-iam/update-account-password-policy/package.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-iam/update-account-password-policy/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-kms/key-lookup.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-macie/create-member/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-macie/create-member/package.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-macie/create-member/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-macie/enable-macie/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-macie/enable-macie/package.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-macie/enable-macie/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-macie/enable-organization-admin-account/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-macie/enable-organization-admin-account/package.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-macie/enable-organization-admin-account/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-macie/macie-export-config-classification.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-macie/macie-members.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-macie/macie-organization-admin-account.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-macie/macie-session.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-macie/put-export-config-classification/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-macie/put-export-config-classification/package.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-macie/put-export-config-classification/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-networkfirewall/firewall.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-networkfirewall/get-network-firewall-endpoint.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-networkfirewall/get-network-firewall-endpoint/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-networkfirewall/get-network-firewall-endpoint/package.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-networkfirewall/get-network-firewall-endpoint/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-networkfirewall/policy.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-networkfirewall/rule-group.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-organizations/account.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-organizations/attach-policy/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-organizations/attach-policy/package.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-organizations/attach-policy/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-accounts-status/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-accounts-status/package.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-accounts-status/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-accounts.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-accounts/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-accounts/package.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-accounts/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-organizational-units/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-organizational-units/package.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-organizational-units/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-policy/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-policy/package.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-policy/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-organizations/describe-organization/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-organizations/describe-organization/package.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-organizations/describe-organization/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-organizations/enable-aws-service-access.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-organizations/enable-aws-service-access/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-organizations/enable-aws-service-access/package.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-organizations/enable-aws-service-access/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-organizations/enable-policy-type.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-organizations/enable-policy-type/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-organizations/enable-policy-type/package.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-organizations/enable-policy-type/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-organizations/invite-account-to-organization/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-organizations/invite-account-to-organization/package.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-organizations/invite-account-to-organization/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-organizations/organization.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-organizations/organizational-units.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-organizations/policy-attachment.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-organizations/policy.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-organizations/register-delegated-administrator.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-organizations/register-delegated-administrator/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-organizations/register-delegated-administrator/package.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-organizations/register-delegated-administrator/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ram/enable-sharing-with-aws-organization.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ram/enable-sharing-with-aws-organization/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ram/enable-sharing-with-aws-organization/package.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ram/enable-sharing-with-aws-organization/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ram/get-resource-share-item/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ram/get-resource-share-item/package.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ram/get-resource-share-item/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ram/get-resource-share/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ram/get-resource-share/package.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ram/get-resource-share/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ram/resource-share.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/endpoint-addresses.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/firewall-domain-list.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/firewall-rule-group.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/get-domain-lists/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/get-domain-lists/package.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/get-domain-lists/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/get-endpoint-addresses/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/get-endpoint-addresses/package.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/get-endpoint-addresses/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/query-logging-config.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/resolver-endpoint.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/resolver-rule.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-route-53/associate-hosted-zones.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-route-53/associate-hosted-zones/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-route-53/associate-hosted-zones/package.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-route-53/associate-hosted-zones/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-route-53/hosted-zone.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-route-53/record-set.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-s3/bucket.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-s3/central-logs-bucket.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-s3/public-access-block.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-s3/put-public-access-block/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-s3/put-public-access-block/package.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-s3/put-public-access-block/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-securityhub/batch-enable-standards/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-securityhub/batch-enable-standards/package.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-securityhub/batch-enable-standards/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-securityhub/create-members/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-securityhub/create-members/package.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-securityhub/create-members/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-securityhub/enable-organization-admin-account/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-securityhub/enable-organization-admin-account/package.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-securityhub/enable-organization-admin-account/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-securityhub/securityhub-members.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-securityhub/securityhub-organization-admin-account.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-securityhub/securityhub-standards.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-servicecatalog/get-portfolio-id.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-servicecatalog/get-portfolio-id/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-servicecatalog/get-portfolio-id/package.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-servicecatalog/get-portfolio-id/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ssm/document.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ssm/get-param-value/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ssm/get-param-value/package.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ssm/get-param-value/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ssm/put-param-value/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ssm/put-param-value/package.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ssm/put-param-value/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ssm/session-manager-settings.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ssm/session-manager-settings/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ssm/session-manager-settings/package.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ssm/session-manager-settings/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ssm/share-document/index.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ssm/share-document/package.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ssm/share-document/tsconfig.json create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ssm/ssm-parameter-lookup.ts create mode 100644 source/packages/@aws-accelerator/constructs/lib/aws-ssm/ssm-parameter.ts create mode 100644 source/packages/@aws-accelerator/constructs/package.json create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-cur/report-definition.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-ec2/__snapshots__/dhcp-options.test.ts.snap create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-ec2/__snapshots__/prefix-list.test.ts.snap create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-ec2/__snapshots__/route-table.test.ts.snap create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-ec2/__snapshots__/transit-gateway-route-table.test.ts.snap create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-ec2/__snapshots__/transit-gateway.test.ts.snap create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-ec2/__snapshots__/vpc-endpoint.test.ts.snap create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-ec2/__snapshots__/vpc.test.ts.snap create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-ec2/delete-default-vpc.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-ec2/dhcp-options.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-ec2/prefix-list.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-ec2/route-table.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-ec2/transit-gateway-route-table.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-ec2/transit-gateway.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-ec2/vpc-endpoint.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-ec2/vpc-peering.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-ec2/vpc.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-guardduty/guardduty-detector-config.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-guardduty/guardduty-members.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-guardduty/guardduty-organization-admin-account.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-guardduty/guardduty-publishing-destination.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-iam/password-policy.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-macie/macie-members.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-macie/macie-organization-admin-account.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-macie/macie-session.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-networkfirewall/firewall.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-networkfirewall/get-network-firewall-endpoint.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-networkfirewall/policy.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-networkfirewall/rule-group.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-organizations/account.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-organizations/enable-aws-service-access.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-organizations/enable-policy-type.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-organizations/organization.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-organizations/organizational-units.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-organizations/policy-attachment.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-organizations/policy.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-organizations/register-delegated-administrator.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-ram/__snapshots__/resource-share.test.ts.snap create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-ram/enable-sharing-with-aws-organization.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-ram/resource-share.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-route-53-resolver/endpoint-addresses.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-route-53-resolver/firewall-domain-list.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-route-53-resolver/firewall-rule-group.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-route-53-resolver/query-logging-config.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-route-53-resolver/resolver-endpoint.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-route-53-resolver/resolver-rule.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-route-53/__snapshots__/hosted-zone.test.ts.snap create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-route-53/__snapshots__/record-set.test.ts.snap create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-route-53/hosted-zone.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-route-53/record-set.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-s3/__snapshots__/central-logs-buckets.test.ts.snap create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-s3/central-logs-buckets.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-s3/public-access-block.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-securityhub/securityhub-members.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-securityhub/securityhub-organization-admin-account.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-securityhub/securityhub-standards.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-ssm/session-manager-settings.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/test/aws-ssm/ssm-parameter-lookup.test.ts create mode 100644 source/packages/@aws-accelerator/constructs/tsconfig.json create mode 100644 source/packages/@aws-accelerator/installer/.npmignore create mode 100644 source/packages/@aws-accelerator/installer/README.md create mode 100644 source/packages/@aws-accelerator/installer/bin/installer.ts create mode 100644 source/packages/@aws-accelerator/installer/cdk.json create mode 100644 source/packages/@aws-accelerator/installer/index.ts create mode 100644 source/packages/@aws-accelerator/installer/jest.config.js create mode 100644 source/packages/@aws-accelerator/installer/lib/installer-stack.ts create mode 100644 source/packages/@aws-accelerator/installer/lib/solutions-helper.ts create mode 100644 source/packages/@aws-accelerator/installer/package.json create mode 100644 source/packages/@aws-accelerator/installer/test/__snapshots__/installer.test.ts.snap create mode 100644 source/packages/@aws-accelerator/installer/test/installer.test.ts create mode 100644 source/packages/@aws-accelerator/installer/tsconfig.json create mode 100644 source/packages/@aws-accelerator/tester/.npmignore create mode 100644 source/packages/@aws-accelerator/tester/README.md create mode 100644 source/packages/@aws-accelerator/tester/bin/app.ts create mode 100644 source/packages/@aws-accelerator/tester/cdk.json create mode 100644 source/packages/@aws-accelerator/tester/index.ts create mode 100644 source/packages/@aws-accelerator/tester/jest.config.js create mode 100644 source/packages/@aws-accelerator/tester/lambdas/index.ts create mode 100644 source/packages/@aws-accelerator/tester/lambdas/package.json create mode 100644 source/packages/@aws-accelerator/tester/lambdas/test-target-functions/validate-transit-gateway.ts create mode 100644 source/packages/@aws-accelerator/tester/lambdas/tsconfig.json create mode 100644 source/packages/@aws-accelerator/tester/lib/tester-stack.ts create mode 100644 source/packages/@aws-accelerator/tester/package.json create mode 100644 source/packages/@aws-accelerator/tester/test/configs/config.yaml create mode 100644 source/packages/@aws-accelerator/tester/test/external-pipeline-account-tester-stack.test.ts create mode 100644 source/packages/@aws-accelerator/tester/test/tester-stack.test.ts create mode 100644 source/packages/@aws-accelerator/tester/tsconfig.json create mode 100644 source/packages/@aws-accelerator/tools/index.ts create mode 100644 source/packages/@aws-accelerator/tools/lib/classes/accelerator-tool.ts create mode 100644 source/packages/@aws-accelerator/tools/package.json create mode 100644 source/packages/@aws-accelerator/tools/test/uninstaller.test.ts create mode 100644 source/packages/@aws-accelerator/tools/tsconfig.json create mode 100644 source/packages/@aws-accelerator/tools/uninstaller.ts create mode 100644 source/packages/@aws-accelerator/utils/README.md create mode 100644 source/packages/@aws-accelerator/utils/index.ts create mode 100644 source/packages/@aws-accelerator/utils/lib/throttle.ts create mode 100644 source/packages/@aws-accelerator/utils/package.json create mode 100644 source/packages/@aws-accelerator/utils/test/throttle.test.ts create mode 100644 source/packages/@aws-accelerator/utils/tsconfig.json create mode 100644 source/packages/@aws-cdk-extensions/cdk-extensions/.gitignore create mode 100644 source/packages/@aws-cdk-extensions/cdk-extensions/.npmignore create mode 100644 source/packages/@aws-cdk-extensions/cdk-extensions/README.md create mode 100644 source/packages/@aws-cdk-extensions/cdk-extensions/index.ts create mode 100644 source/packages/@aws-cdk-extensions/cdk-extensions/jest.config.js create mode 100644 source/packages/@aws-cdk-extensions/cdk-extensions/lib/repository.ts create mode 100644 source/packages/@aws-cdk-extensions/cdk-extensions/lib/trail.ts create mode 100644 source/packages/@aws-cdk-extensions/cdk-extensions/package.json create mode 100644 source/packages/@aws-cdk-extensions/cdk-extensions/test/__snapshots__/repository-snapshot.test.ts.snap create mode 100644 source/packages/@aws-cdk-extensions/cdk-extensions/test/__snapshots__/repository.test.ts.snap create mode 100644 source/packages/@aws-cdk-extensions/cdk-extensions/test/repository-fine-grained.test.ts create mode 100644 source/packages/@aws-cdk-extensions/cdk-extensions/test/repository-snapshot.test.ts create mode 100644 source/packages/@aws-cdk-extensions/cdk-extensions/test/repository.test.ts create mode 100644 source/packages/@aws-cdk-extensions/cdk-extensions/test/test-config.ts create mode 100644 source/packages/@aws-cdk-extensions/cdk-extensions/tsconfig.json create mode 100644 source/packages/@aws-cdk-extensions/cdk-plugin-assume-role/.gitignore create mode 100644 source/packages/@aws-cdk-extensions/cdk-plugin-assume-role/index.ts create mode 100644 source/packages/@aws-cdk-extensions/cdk-plugin-assume-role/lib/assume-role-plugin.ts create mode 100644 source/packages/@aws-cdk-extensions/cdk-plugin-assume-role/lib/assume-role-provider-source.ts create mode 100644 source/packages/@aws-cdk-extensions/cdk-plugin-assume-role/lib/backoff.ts create mode 100644 source/packages/@aws-cdk-extensions/cdk-plugin-assume-role/package.json create mode 100644 source/packages/@aws-cdk-extensions/cdk-plugin-assume-role/tsconfig.json create mode 100755 source/run-all-tests.sh create mode 100644 source/tsconfig.json create mode 100644 source/yarn.lock diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100755 index 000000000..7233e10dd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,42 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior. + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Please complete the following information about the solution:** +- [ ] Version: [e.g. v1.0.0] + +To get the version of the solution, you can look at the description of the created CloudFormation stack. For example, "_(SO0021) - Video On Demand workflow with AWS Step Functions, MediaConvert, MediaPackage, S3, CloudFront and DynamoDB. Version **v5.0.0**_". If the description does not contain the version information, you can look at the mappings section of the template: + +```yaml +Mappings: + SourceCode: + General: + S3Bucket: "solutions" + KeyPrefix: "video-on-demand-on-aws/v5.0.0" +``` + +- [ ] Region: [e.g. us-east-1] +- [ ] Was the solution modified from the version published on this repository? +- [ ] If the answer to the previous question was yes, are the changes available on GitHub? +- [ ] Have you checked your [service quotas](https://docs.aws.amazon.com/general/latest/gr/aws_service_limits.html) for the sevices this solution uses? +- [ ] Were there any errors in the CloudWatch Logs? + +**Screenshots** +If applicable, add screenshots to help explain your problem (please **DO NOT include sensitive information**). + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100755 index 000000000..d3d209faa --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,17 @@ +--- +name: Feature request +about: Suggest an idea for this solution +title: '' +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the feature you'd like** +A clear and concise description of what you want to happen. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100755 index 000000000..de50e4d9a --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,5 @@ +*Issue #, if available:* + +*Description of changes:* + +By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. diff --git a/.gitignore b/.gitignore new file mode 100755 index 000000000..647d41039 --- /dev/null +++ b/.gitignore @@ -0,0 +1,59 @@ +*.js +!deployment/.typescript/cdk-solution-helper/index.js +package-lock.json +!jest.config.js +*.d.ts +*.tsbuildinfo +node_modules +dist +!source/.typescript/lambda/**/*.js +!source/lambda/**/*.js + +### log files +*.log + +### CDK asset staging directory +.cdk.staging +cdk.out + +!**/cdk-solution-helper/index.js + +deployment/global-s3-assets +deployment/regional-s3-assets +open-source/ + +### Temporary folders +tmp/ +temp/ + +### yarn files +yarn-error.log + +### jest coverage folder +coverage/ + +### VisualStudioCode ### +.vscode/* + +### macOS ### +.DS_Store + +### IntelliJ ### +.idea + +### Exclude Documentation ### +docs + +### Exclude Jest test report directory +test-reports +source/lerna-debug.log + +### Logger files +*.log + +### Debugging +development +.vscode + +### Viperlight +viperlight.zip \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100755 index 000000000..5406ecd71 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +# Change Log + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] - 2022-05-23 + +### Added + +- All files, initial version diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100755 index 000000000..ec98f2b76 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,5 @@ +## Code of Conduct + +This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). +For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact +opensource-codeofconduct@amazon.com with any additional questions or comments. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100755 index 000000000..b2445e134 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,62 @@ +# Contributing Guidelines + +Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional +documentation, we greatly value feedback and contributions from our community. + +Please read through this document before submitting any issues or pull requests to ensure we have all the necessary +information to effectively respond to your bug report or contribution. + + +## Reporting Bugs/Feature Requests + +We welcome you to use the GitHub issue tracker to report bugs or suggest features. + +When filing an issue, please check [existing open](https://github.com/awslabs/aws-compliance-accelerator/issues), or [recently closed](https://github.com/awslabs/aws-compliance-accelerator/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already +reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: + +* A reproducible test case or series of steps +* The version of our code being used +* Any modifications you've made relevant to the bug +* Anything unusual about your environment or deployment + + +## Contributing via Pull Requests +Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: + +1. You are working against the latest source on the *master* branch. +2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. +3. You open an issue to discuss any significant work - we would hate for your time to be wasted. + +To send us a pull request, please: + +1. Fork the repository. +2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. +3. Ensure all build processes execute successfully (see README.md for additional guidance). +4. Ensure all unit, integration, and/or snapshot tests pass, as applicable. +5. Commit to your fork using clear commit messages. +6. Send us a pull request, answering any default questions in the pull request interface. +7. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. + +GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and +[creating a pull request](https://help.github.com/articles/creating-a-pull-request/). + + +## Finding contributions to work on +Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels ((enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/awslabs/aws-compliance-accelerator/labels/help%20wanted) issues is a great place to start. + + +## Code of Conduct +This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). +For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact +opensource-codeofconduct@amazon.com with any additional questions or comments. + + +## Security issue notifications +If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public GitHub issue. + + +## Licensing + +See the [LICENSE](https://github.com/awslabs/aws-compliance-accelerator/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. + +We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..67db85882 --- /dev/null +++ b/LICENSE @@ -0,0 +1,175 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100755 index 000000000..19dc35b24 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,175 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. \ No newline at end of file diff --git a/NOTICE b/NOTICE new file mode 100644 index 000000000..616fc5889 --- /dev/null +++ b/NOTICE @@ -0,0 +1 @@ +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/NOTICE.txt b/NOTICE.txt new file mode 100755 index 000000000..8aa9df315 --- /dev/null +++ b/NOTICE.txt @@ -0,0 +1,21 @@ +Landing Zone Accelerator on AWS + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +Licensed under the Apache License Version 2.0 (the "License"). You may not use this file except +in compliance with the License. A copy of the License is located at http://www.apache.org/licenses/ +or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or implied. See the License for the +specific language governing permissions and limitations under the License. + +********************** +THIRD PARTY COMPONENTS +********************** +This software includes third party software subject to the following copyrights: + +AWS CDK - Apache-2.0 +AWS SDK - Apache-2.0 +jest - MIT license +node - MIT license +ts-jest - MIT license +ts-node - MIT license +typescript - Apache-2.0 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100755 index 000000000..0824ebf15 --- /dev/null +++ b/README.md @@ -0,0 +1,134 @@ +# Landing Zone Accelerator on AWS + +The Landing Zone Accelerator on AWS solution helps you quickly deploy a secure, +resilient, scalable, and fully automated cloud foundation that accelerates your +readiness for your cloud compliance program. A landing zone is a cloud +environment that offers a recommended starting point, including default +accounts, account structure, network and security layouts, and so forth. From a +landing zone, you can deploy workloads that utilize your solutions and +applications. + +The Landing Zone Accelerator (LZA) is architected to align with AWS best +practices and in conformance with multiple, global compliance frameworks. When +used in coordination with services such as AWS Control Tower, the Landing Zone +Accelerator provides a comprehensive no-code solution across 35+ AWS services to +manage and govern a multi-account environment built to support customers with +highly-regulated workloads and complex compliance requirements. The LZA helps +you establish platform readiness with security, compliance, and operational +capabilities. + +This solution is provided as an open-source project that is built using the AWS +Cloud Development Kit (CDK). You install directly into your environment giving +you full access to the infrastructure as code (IaC) solution. Through a +simplified set of configuration files, you are able to configure additional +functionality, guardrails and security services (eg. AWS Managed Config Rules, +and AWS SecurityHub), manage your foundational networking topology (eg. VPCs, +Transit Gateways, and Network Firewall), and generate additional workload +accounts using the AWS Control Tower Account Factory. + +There are no additional charges or upfront commitments required to use Landing +Zone Accelerator on AWS. You pay only for AWS services enabled in order to set +up your platform and operate your guardrails. This solution can also support +non-standard AWS partitions, including AWS GovCloud (US), and the US Secret and +Top Secret regions. + +For an overview and solution deployment guide, please visit +[Landing Zone Accelerator on AWS](https://aws.amazon.com/solutions/implementations/landing-zone-accelerator-on-aws/) + +--- + +IMPORTANT: This solution will not, by itself, make you compliant. It provides +the foundational infrastructure from which additional complementary solutions +can be integrated. The information contained in this solution implementation +guide is not exhaustive. You must be review, evaluate, assess, and approve the +solution in compliance with your organization’s particular security features, +tools, and configurations. It is the sole responsibility of you and your +organization to determine which regulatory requirements are applicable and to +ensure that you comply with all requirements. Although this solution discusses +both the technical and administrative requirements, this solution does not help +you comply with the non-technical administrative requirements. + +--- + +--- + +## Package Structure + +### @aws-accelerator/accelerator + +A CDK Application. The core of the accelerator solution. Contains all the stack +definitions and deployment pipeline for the accelerator. This also includes the +CDK Toolkit orchestration. + +### @aws-accelerator/config + +A pure typescript library containing modules to manage the accelerator config +files. + +### @aws-accelerator/constructs + +Contains L2/L3 constructs that have been built to support accelerator actions, +such as creating an AWS Organizational Unit or VPC. These constructs are +intended to be fully reusable, independent of the accelerator, and do not +directly access the accelerator configuration files. Example: CentralLogsBucket, +an S3 bucket that is configured with a CMK with the proper key and bucket +policies to allow services and accounts in the organization to publish logs to +the bucket. + +### @aws-accelerator/installer + +Contains a CDK Application that defines the accelerator Installer stack. + +### @aws-accelerator/ui (future) + +A web application that utilizes the aws-ui-components library to present a +console to configure the accelerator + +### @aws-accelerator/utils + +Contains common utilities and types that are needed by @aws-accelerator/\* +packages. For example, throttling and backoff for AWS SDK calls + +### @aws-cdk-extensions/cdk-extensions + +Contains L2 constructs that extend the functionality of the CDK repo. The CDK +repo is an actively developed project. As the accelerator team identifies +missing features of the CDK, those features will be initially developed locally +within this repo and submitted to the CDK project as a pull request. + +### @aws-cdk-extensions/tester + +Accelerator tester CDK app. This package creates AWS Config custom rules for every test cases defined in test case manifest file. + +--- + +## Included Features + +| Service / Feature | Resource | Details | +| ------------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| AWS Control Tower | Control Tower | Enabled in the global-config.yaml. It is recommended that AWS Control Tower is enabled, if available, in the desired home region for your environment prior to installing the accelerator. When enabled, the accelerator will integrate with resources and guardrails deployed by AWS Control Tower. | +| AWS Config | Config Recorder | The accelerator configures AWS Config Recorders in all specified accounts and regions | +| AWS Config | Config Rules | Defined in the security-config.yaml and deployed to all specified accounts and regions as individual account Config Rules. Support for Organizations Config Rules is planned for a future version | +| AWS Organizations | Organizational Units | Defined in the organization-config.yaml and deployed through the management (root) in the home region | +| AWS Organizations | Service Control Policies | Defined in the organization-config.yaml and deployed through the management (root) in the home region | +| AWS SecurityHub | SecurityHub | Defined in the security-config.yaml and deployed to all specified accounts and regions. Additionally, the accelerator will designate a service administrator account, commonly this is the security audit account | +| Amazon Macie | Macie Session | Defined in the security-config.yaml and deployed to all specified accounts and regions. Additionally, the accelerator will designate a service administrator account, commonly this is the security audit account | +| Amazon GuardDuty | GuardDuty | Defined in the security-config.yaml and deployed to all specified accounts and regions. Additionally, the accelerator will designate a service administrator account, commonly this is the security audit account | +| AWS Cloudtrail | Organizations Trail | Defined in the global-config.yaml. When specified, an Organizations trail is deployed through the management (root) account to cover all regions, and all trails are recorded to the central-logging-bucket defined in the log-archive account. | +| Centralized Logging | S3 | Defined in the global-config.yaml, integrates with AWS Control Tower, if enabled, to centralize logs from AWS services, such as AWS CloudTrail, AWS Config and VPC FlowLogs | +| AWS IAM | Policies / Roles / Groups / Users | Defined in the iam-config.yaml and deployed to all specified accounts and regions. The accelerator will integrate an identity provider (IdP) metadata document can be stored in AWS CUsers that are specified in the configuration are created with AWS Secrets Manager generated passwords and stored locally in the account where the user was created. | +| AWS IAM | SAML Federation | Defined in the iam-config.yaml and deployed to all specified accounts and regions. The accelerator will integrate the specified identity provider (IdP) metadata document with AWS IAM. | +| Core Networking | VPC / Subnets / Route Tables / Security Groups/ NACLs | Defined in the network-config.yaml and deployed to all specified accounts and regions | +| Core Networking | Transit Gateway | Defined in the network-config.yaml and deployed to all specified accounts and regions. The accelerator will automatically attach VPCs to specified Transit Gateways | +| Core Networking | VPC Endpoints | Defined in the network-config.yaml and deployed to all specified accounts and regions. The accelerator will also deploy AWS Route53 Hosted Zones to specified VPCs to support centralized VPC endpoint usage | +| Core Networking | VPC Flow Logs | Defined in the network-config.yaml and deployed to all specified accounts and regions. VPC Flow Logs can be configured on all defined VPCs to send to S3 for centralized logging and/or CloudWatch Logs | + +--- + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +Licensed under the Apache License Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at + + http://www.apache.org/licenses/ + +or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/deployment/build-open-source-dist.sh b/deployment/build-open-source-dist.sh new file mode 100755 index 000000000..2641bd18f --- /dev/null +++ b/deployment/build-open-source-dist.sh @@ -0,0 +1,132 @@ +#!/bin/bash +# +# This script packages your project into an open-source solution distributable +# that can be published to sites like GitHub. +# +# Important notes and prereq's: +# 1. The initialize-repo.sh script must have been run in order for this script to +# function properly. +# 2. This script should be run from the repo's /deployment folder. +# +# This script will perform the following tasks: +# 1. Remove any old dist files from previous runs. +# 2. Package the GitHub contribution and pull request templates (typically +# found in the /.github folder). +# 3. Package the /source folder along with the necessary root-level +# open-source artifacts (i.e. CHANGELOG, etc.). +# 4. Remove any unecessary artifacts from the /open-source folder (i.e. +# node_modules, package-lock.json, etc.). +# 5. Zip up the /open-source folder and create the distributable. +# 6. Remove any temporary files used for staging. +# +# Parameters: +# - solution-name: name of the solution for consistency + +# Check to see if the required parameters have been provided: +if [ -z "$1" ]; then + echo "Please provide the trademark approved solution name for the open source package." + echo "For example: ./build-open-source-dist.sh trademarked-solution-name" + exit 1 +fi + +# Get reference for all important folders +source_template_dir="$PWD" +dist_dir="$source_template_dir/open-source" +source_dir="$source_template_dir/../source" +github_dir="$source_template_dir/../.github" +deployment_dir="$source_template_dir/../deployment" +reference_dir="$source_template_dir/../reference" + +echo "------------------------------------------------------------------------------" +echo "[Init] Remove any old dist files from previous runs" +echo "------------------------------------------------------------------------------" + +echo "rm -rf $dist_dir" +rm -rf $dist_dir +echo "mkdir -p $dist_dir" +mkdir -p $dist_dir + +echo "------------------------------------------------------------------------------" +echo "[Packing] GitHub templates" +echo "------------------------------------------------------------------------------" + +echo "cp -r $github_dir $dist_dir" +cp -r $github_dir $dist_dir + +echo "------------------------------------------------------------------------------" +echo "[Packing] Source folder" +echo "------------------------------------------------------------------------------" + +echo "cp -r $source_dir $dist_dir" +cp -r $source_dir $dist_dir + + +echo "------------------------------------------------------------------------------" +echo "[Packing] Deployment folder" +echo "------------------------------------------------------------------------------" + +echo "cp -r $deployment_dir $dist_dir" +cp -r $deployment_dir $dist_dir + +echo "------------------------------------------------------------------------------" +echo "[Packing] Reference folder" +echo "------------------------------------------------------------------------------" + +echo "cp -r $reference_dir $dist_dir" +cp -r $reference_dir $dist_dir + +echo "------------------------------------------------------------------------------" +echo "[Packing] Files from the root level of the project" +echo "------------------------------------------------------------------------------" + +echo "cp $source_template_dir/../LICENSE.txt $dist_dir" +cp $source_template_dir/../LICENSE.txt $dist_dir + +echo "cp $source_template_dir/../NOTICE.txt $dist_dir" +cp $source_template_dir/../NOTICE.txt $dist_dir + +echo "cp $source_template_dir/../README.md $dist_dir" +cp $source_template_dir/../README.md $dist_dir + +echo "cp $source_template_dir/../CODE_OF_CONDUCT.md $dist_dir" +cp $source_template_dir/../CODE_OF_CONDUCT.md $dist_dir + +echo "cp $source_template_dir/../CONTRIBUTING.md $dist_dir" +cp $source_template_dir/../CONTRIBUTING.md $dist_dir + +echo "cp $source_template_dir/../CHANGELOG.md $dist_dir" +cp $source_template_dir/../CHANGELOG.md $dist_dir + +echo "cp $source_template_dir/../.gitignore $dist_dir" +cp $source_template_dir/../.gitignore $dist_dir + +echo "------------------------------------------------------------------------------" +echo "[Packing] Clean up the open-source distributable" +echo "------------------------------------------------------------------------------" +echo $dist_dir +# General cleanup of node_modules and package-lock.json files +echo "find $dist_dir -iname "node_modules" -type d -exec rm -rf "{}" \; 2> /dev/null" +find $dist_dir -iname "node_modules" -type d -exec rm -rf "{}" \; 2> /dev/null +echo "find $dist_dir -iname "package-lock.json" -type f -exec rm -f "{}" \; 2> /dev/null" +find $dist_dir -iname "package-lock.json" -type f -exec rm -f "{}" \; 2> /dev/null + +echo "------------------------------------------------------------------------------" +echo "[Packing] Create GitHub (open-source) zip file" +echo "------------------------------------------------------------------------------" + +# Create the zip file +echo "cd $dist_dir" +cd $dist_dir +echo "zip -q -r9 ../$1.zip ." +zip -q -r9 ../$1.zip . + +# Cleanup any temporary/unnecessary files +echo "Clean up open-source folder" +echo "rm -rf * .*" +rm -rf * .* + +# Place final zip file in $dist_dir +echo "mv ../$1.zip ." +mv ../$1.zip . + +echo "Completed building $1.zip dist" \ No newline at end of file diff --git a/deployment/build-s3-dist.sh b/deployment/build-s3-dist.sh new file mode 100755 index 000000000..20ba38c78 --- /dev/null +++ b/deployment/build-s3-dist.sh @@ -0,0 +1,408 @@ +#!/bin/bash +# +# This script packages your project into a solution distributable that can be +# used as an input to the solution builder validation pipeline. +# +# Important notes and prereq's: +# 1. The initialize-repo.sh script must have been run in order for this script to +# function properly. +# 2. This script should be run from the repo's /deployment folder. +# +# This script will perform the following tasks: +# 1. Remove any old dist files from previous runs. +# 2. Install dependencies for the cdk-solution-helper; responsible for +# converting standard 'cdk synth' output into solution assets. +# 3. Build and synthesize your CDK project. +# 4. Run the cdk-solution-helper on template outputs and organize +# those outputs into the /global-s3-assets folder. +# 5. Organize source code artifacts into the /regional-s3-assets folder. +# 6. Remove any temporary files used for staging. +# +# Parameters: +# - source-bucket-base-name: Name for the S3 bucket location where the template will source the Lambda +# code from. The template will append '-[region_name]' to this bucket name. +# For example: ./build-s3-dist.sh solutions v1.0.0 +# The template will then expect the source code to be located in the solutions-[region_name] bucket +# - solution-name: name of the solution for consistency +# - version-code: version of the package +#----------------------- +# Formatting +bold=$(tput bold) +normal=$(tput sgr0) +#------------------------------------------------------------------------------ +# SETTINGS +#------------------------------------------------------------------------------ +template_format="json" +run_helper="true" + +# run_helper is false for yaml - not supported +[[ $template_format == "yaml" ]] && { + run_helper="false" + echo "${bold}Solution_helper disabled:${normal} template format is yaml" +} + +#------------------------------------------------------------------------------ +# DISABLE OVERRIDE WARNINGS +#------------------------------------------------------------------------------ +# Use with care: disables the warning for overridden properties on +# AWS Solutions Constructs +export overrideWarningsEnabled=false + +#------------------------------------------------------------------------------ +# Build Functions +#------------------------------------------------------------------------------ +# Echo, execute, and check the return code for a command. Exit if rc > 0 +# ex. do_cmd npm run build +usage() +{ + echo "Usage: $0 bucket solution-name version" + echo "Please provide the base source bucket name, trademarked solution name, and version." + echo "For example: ./build-s3-dist.sh mybucket my-solution v1.0.0" + exit 1 +} + +do_cmd() +{ + echo "------ EXEC $*" + $* + rc=$? + if [ $rc -gt 0 ] + then + echo "Aborted - rc=$rc" + exit $rc + fi +} + +sedi() +{ + # cross-platform for sed -i + sed -i $* 2>/dev/null || sed -i "" $* +} + +# use sed to perform token replacement +# ex. do_replace myfile.json %%VERSION%% v1.1.1 +do_replace() +{ + replace="s/$2/$3/g" + file=$1 + do_cmd sedi $replace $file +} + +create_template_json() +{ + # Run 'cdk synth' to generate raw solution outputs + do_cmd yarn run cdk synth --output=$staging_dist_dir + + # Remove unnecessary output files + do_cmd cd $staging_dist_dir + # ignore return code - can be non-zero if any of these does not exist + rm tree.json manifest.json cdk.out + + # Move outputs from staging to template_dist_dir + echo "Move outputs from staging to template_dist_dir" + do_cmd mv $staging_dist_dir/*.template.json $template_dist_dir/ + + # Rename all *.template.json files to *.template + echo "Rename all *.template.json to *.template" + echo "copy templates and rename" + for f in $template_dist_dir/*.template.json; do + mv -- "$f" "${f%.template.json}.template" + done +} + +create_template_yaml() +{ + # Assumes current working directory is where the CDK is defined + # Output YAML - this is currently the only way to do this for multiple templates + maxrc=0 + for template in `cdk list`; do + echo Create template $template + do_cmd yarn run cdk synth $template > ${template_dist_dir}/${template}.template + if [[ $? > $maxrc ]]; then + maxrc=$? + fi + done +} + +cleanup_temporary_generted_files() +{ + echo "------------------------------------------------------------------------------" + echo "${bold}[Cleanup] Remove temporary files${normal}" + echo "------------------------------------------------------------------------------" + + # Delete generated files: CDK Consctruct typescript transcompiled generted files + do_cmd cd $source_dir/ + do_cmd yarn run cleanup:tsc + + # Delete the temporary /staging folder + do_cmd rm -rf $staging_dist_dir +} + +fn_exists() +{ + exists=`LC_ALL=C type $1` + return $? +} + +#------------------------------------------------------------------------------ +# INITIALIZATION +#------------------------------------------------------------------------------ +# solution_config must exist in the deployment folder (same folder as this +# file) . It is the definitive source for solution ID, name, and trademarked +# name. +# +# Example: +# +# SOLUTION_ID='SO0111' +# SOLUTION_NAME='AWS Security Hub Automated Response & Remediation' +# SOLUTION_TRADEMARKEDNAME='aws-security-hub-automated-response-and-remediation' +# SOLUTION_VERSION='v1.1.1' # optional +if [[ -e './solution_config' ]]; then + source ./solution_config +else + echo "solution_config is missing from the solution root." + exit 1 +fi + +if [[ -z $SOLUTION_ID ]]; then + echo "SOLUTION_ID is missing from ../solution_config" + exit 1 +else + export SOLUTION_ID +fi + +if [[ -z $SOLUTION_NAME ]]; then + echo "SOLUTION_NAME is missing from ../solution_config" + exit 1 +else + export SOLUTION_NAME +fi + +if [[ -z $SOLUTION_TRADEMARKEDNAME ]]; then + echo "SOLUTION_TRADEMARKEDNAME is missing from ../solution_config" + exit 1 +else + export SOLUTION_TRADEMARKEDNAME +fi + +if [[ ! -z $SOLUTION_VERSION ]]; then + export SOLUTION_VERSION +fi + +#------------------------------------------------------------------------------ +# Validate command line parameters +#------------------------------------------------------------------------------ +# Validate command line input - must provide bucket +[[ -z $1 ]] && { usage; exit 1; } || { SOLUTION_BUCKET=$1; } + +# Environmental variables for use in CDK +export DIST_OUTPUT_BUCKET=$SOLUTION_BUCKET + +# Version from the command line is definitive. Otherwise, use, in order of precedence: +# - SOLUTION_VERSION from solution_config +# - version.txt +# +# Note: Solutions Pipeline sends bucket, name, version. Command line expects bucket, version +# if there is a 3rd parm then version is $3, else $2 +# +# If confused, use build-s3-dist.sh +if [ ! -z $3 ]; then + version="$3" +elif [ ! -z "$2" ]; then + version=$2 +elif [ ! -z $SOLUTION_VERSION ]; then + version=$SOLUTION_VERSION +elif [ -e ../source/version.txt ]; then + version=`cat ../source/version.txt` +else + echo "Version not found. Version must be passed as an argument or in version.txt in the format vn.n.n" + exit 1 +fi +SOLUTION_VERSION=$version + +# SOLUTION_VERSION should be vn.n.n +if [[ $SOLUTION_VERSION != v* ]]; then + echo prepend v to $SOLUTION_VERSION + SOLUTION_VERSION=v${SOLUTION_VERSION} +fi + +export SOLUTION_VERSION=$version + +#----------------------------------------------------------------------------------- +# Get reference for all important folders +#----------------------------------------------------------------------------------- +template_dir="$PWD" +staging_dist_dir="$template_dir/staging" +template_dist_dir="$template_dir/global-s3-assets" +build_dist_dir="$template_dir/regional-s3-assets" +source_dir="$template_dir/../source" +installer_dir="$source_dir/packages/@aws-accelerator/installer" + +echo "------------------------------------------------------------------------------" +echo "${bold}[Init] Remove any old dist files from previous runs${normal}" +echo "------------------------------------------------------------------------------" + +do_cmd rm -rf $template_dist_dir +do_cmd mkdir -p $template_dist_dir +do_cmd rm -rf $build_dist_dir +do_cmd mkdir -p $build_dist_dir +do_cmd rm -rf $staging_dist_dir +do_cmd mkdir -p $staging_dist_dir + + +echo "------------------------------------------------------------------------------" +echo "${bold}[Init] Install dependencies for the cdk-solution-helper${normal}" +echo "------------------------------------------------------------------------------" + +do_cmd cd $template_dir/cdk-solution-helper +do_cmd npm install + +echo "------------------------------------------------------------------------------" +echo "${bold}[Synth] CDK Project${normal}" +echo "------------------------------------------------------------------------------" + +do_cmd cd $source_dir + +# Install the global lerna package +# Note: do not install using global (-g) option. This makes build-s3-dist.sh difficult +# for customers and developers to use, as it globally changes their environment. +do_cmd yarn add lerna@^4.0.0 -W + +# Add local install to PATH +export PATH=$(yarn bin):$PATH + +do_cmd yarn lerna bootstrap +do_cmd yarn build # build javascript from typescript to validate the code + # cdk synth doesn't always detect issues in the typescript + # and may succeed using old build files. This ensures we + # have fresh javascript from a successful build + + +echo "------------------------------------------------------------------------------" +echo "${bold}[Create] Templates${normal}" +echo "------------------------------------------------------------------------------" + +if fn_exists create_template_${template_format}; then + do_cmd cd $installer_dir + create_template_${template_format} + do_cmd cd $source_dir +else + echo "Invalid setting for \$template_format: $template_format" + exit 255 +fi + +echo "------------------------------------------------------------------------------" +echo "${bold}[Packing] Template artifacts${normal}" +echo "------------------------------------------------------------------------------" + +# Run the helper to clean-up the templates and remove unnecessary CDK elements +echo "Run the helper to clean-up the templates and remove unnecessary CDK elements" +[[ $run_helper == "true" ]] && { + echo "node $template_dir/cdk-solution-helper/index" + node $template_dir/cdk-solution-helper/index + if [ "$?" = "1" ]; then + echo "(cdk-solution-helper) ERROR: there is likely output above." 1>&2 + exit 1 + fi +} || echo "${bold}Solution Helper skipped: ${normal}run_helper=false" + +# Find and replace bucket_name, solution_name, and version +echo "Find and replace bucket_name, solution_name, and version" +cd $template_dist_dir +do_replace "*.template" %%BUCKET_NAME%% ${SOLUTION_BUCKET} +do_replace "*.template" %%SOLUTION_NAME%% ${SOLUTION_TRADEMARKEDNAME} +do_replace "*.template" %%VERSION%% ${SOLUTION_VERSION} + +echo "------------------------------------------------------------------------------" +echo "${bold}[Packing] Source code artifacts${normal}" +echo "------------------------------------------------------------------------------" + +# General cleanup of node_modules files +echo "find $staging_dist_dir -iname "node_modules" -type d -exec rm -rf "{}" \; 2> /dev/null" +find $staging_dist_dir -iname "node_modules" -type d -exec rm -rf "{}" \; 2> /dev/null + +# ... For each asset.* source code artifact in the temporary /staging folder... +cd $staging_dist_dir +for d in `find . -mindepth 1 -maxdepth 1 -type d`; do + + # Rename the artifact, removing the period for handler compatibility + pfname="$(basename -- $d)" + fname="$(echo $pfname | sed -e 's/\.//g')" + echo "zip -r $fname.zip $fname" + mv $d $fname + + # Build the artifacts + if test -f $fname/requirements.txt; then + echo "====================================" + echo "This is Python runtime" + echo "====================================" + cd $fname + venv_folder="./venv-prod/" + rm -fr .venv-test + rm -fr .venv-prod + echo "Initiating virtual environment" + python3 -m venv $venv_folder + source $venv_folder/bin/activate + pip3 install -q -r requirements.txt --target . + deactivate + cd $staging_dist_dir/$fname/$venv_folder/lib/python3.*/site-packages + echo "zipping the artifact" + zip -qr9 $staging_dist_dir/$fname.zip . + cd $staging_dist_dir/$fname + zip -gq $staging_dist_dir/$fname.zip *.py util/* + cd $staging_dist_dir + elif test -f $fname/package.json; then + echo "====================================" + echo "This is Node runtime" + echo "====================================" + cd $fname + echo "Clean and rebuild artifacts" + npm run clean + npm ci + if [ "$?" = "1" ]; then + echo "ERROR: Seems like package-lock.json does not exists or is out of sync with package.json. Trying npm install instead" 1>&2 + npm install + fi + cd $staging_dist_dir + # Zip the artifact + echo "zip -r $fname.zip $fname" + zip -rq $fname.zip $fname + else + echo "====================================" + echo "This is a Directory Asset" + echo "====================================" + + echo "zip -r $fname.zip $fname" + zip -rq $fname.zip $fname + fi + + if test -f $fname.zip; then + # Copy the zipped artifact from /staging to /regional-s3-assets + echo "cp $fname.zip $build_dist_dir" + cp $fname.zip $build_dist_dir + + # Remove the old, unzipped artifact from /staging + echo "rm -rf $fname" + rm -rf $fname + + # Remove the old, zipped artifact from /staging + echo "rm $fname.zip" + rm $fname.zip + # ... repeat until all source code artifacts are zipped and placed in the + # ... /regional-s3-assets folder + else + echo "ERROR: $fname.zip not found" + exit 1 + fi + +done + +# This solution does not generate any assets, need to make a file to move the +# pipeline forward +touch $build_dist_dir/temp-asset.file + +# cleanup temporary generated files that are not needed for later stages of the build pipeline +cleanup_temporary_generted_files + +# Return to original directory from when we started the build +cd $template_dir diff --git a/deployment/cdk-solution-helper/README.md b/deployment/cdk-solution-helper/README.md new file mode 100755 index 000000000..35ef2504f --- /dev/null +++ b/deployment/cdk-solution-helper/README.md @@ -0,0 +1,152 @@ +# cdk-solution-helper + +A lightweight helper function that cleans-up synthesized templates from the AWS Cloud Development Kit (CDK) and prepares +them for use with the AWS Solutions publishing pipeline. This function performs the following tasks: + +#### Lambda function preparation + +Replaces the AssetParameter-style properties that identify source code for Lambda functions with the common variables +used by the AWS Solutions publishing pipeline. + +- `Code.S3Bucket` is assigned the `%%BUCKET_NAME%%` placeholder value. +- `Code.S3Key` is assigned the `%%SOLUTION_NAME%%`/`%%VERSION%%` placeholder value. +- `Handler` is given a prefix identical to the artifact hash, enabling the Lambda function to properly find the handler in the extracted source code package. + +These placeholders are then replaced with the appropriate values using the default find/replace operation run by the pipeline. + +Before: +``` +"examplefunction67F55935": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParametersd513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7S3Bucket54E71A95" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersd513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7S3VersionKeyC789D8B1" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersd513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7S3VersionKeyC789D8B1" + } + ] + } + ] + } + ] + ] + } + }, ... + Handler: "index.handler", ... +``` + +After helper function run: +``` +"examplefunction67F55935": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "%%BUCKET_NAME%%", + "S3Key": "%%SOLUTION_NAME%%/%%VERSION%%/assetd513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7.zip" + }, ... + "Handler": "assetd513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7/index.handler" +``` + +After build script run: +``` +"examplefunction67F55935": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "solutions", + "S3Key": "trademarked-solution-name/v1.0.0/asset.d513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7.zip" + }, ... + "Handler": "assetd513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7/index.handler" +``` + +After CloudFormation deployment: +``` +"examplefunction67F55935": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "solutions-us-east-1", + "S3Key": "trademarked-solution-name/v1.0.0/asset.d513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7.zip" + }, ... + "Handler": "assetd513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7/index.handler" +``` + +#### Template cleanup + +Cleans-up the parameters section and improves readability by removing the AssetParameter-style fields that would have +been used to specify Lambda source code properties. This allows solution-specific parameters to be highlighted and +removes unnecessary clutter. + +Before: +``` +"Parameters": { + "AssetParametersd513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7S3Bucket54E71A95": { + "Type": "String", + "Description": "S3 bucket for asset \"d513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7\"" + }, + "AssetParametersd513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7S3VersionKeyC789D8B1": { + "Type": "String", + "Description": "S3 key for asset version \"d513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7\"" + }, + "AssetParametersd513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7ArtifactHash7AA751FE": { + "Type": "String", + "Description": "Artifact hash for asset \"d513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7\"" + }, + "CorsEnabled" : { + "Description" : "Would you like to enable Cross-Origin Resource Sharing (CORS) for the image handler API? Select 'Yes' if so.", + "Default" : "No", + "Type" : "String", + "AllowedValues" : [ "Yes", "No" ] + }, + "CorsOrigin" : { + "Description" : "If you selected 'Yes' above, please specify an origin value here. A wildcard (*) value will support any origin.", + "Default" : "*", + "Type" : "String" + } + } + ``` + +After: +``` +"Parameters": { + "CorsEnabled" : { + "Description" : "Would you like to enable Cross-Origin Resource Sharing (CORS) for the image handler API? Select 'Yes' if so.", + "Default" : "No", + "Type" : "String", + "AllowedValues" : [ "Yes", "No" ] + }, + "CorsOrigin" : { + "Description" : "If you selected 'Yes' above, please specify an origin value here. A wildcard (*) value will support any origin.", + "Default" : "*", + "Type" : "String" + } + } + ``` + +*** +© Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. \ No newline at end of file diff --git a/deployment/cdk-solution-helper/index.js b/deployment/cdk-solution-helper/index.js new file mode 100755 index 000000000..67ede94fc --- /dev/null +++ b/deployment/cdk-solution-helper/index.js @@ -0,0 +1,130 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +const fs = require('fs'); +const _regex = /[\w]*AssetParameters/g; //this regular express also takes into account lambda functions defined in nested stacks + +// Paths +const global_s3_assets = '../deployment/global-s3-assets'; + +// For each template in global_s3_assets ... +fs.readdirSync(global_s3_assets).forEach(file => { + + // Import and parse template file + const raw_template = fs.readFileSync(`${global_s3_assets}/${file}`); + let template = JSON.parse(raw_template); + + // Clean-up Lambda function code dependencies + const resources = (template.Resources) ? template.Resources : {}; + const lambdaFunctions = Object.keys(resources).filter(function(key) { + return resources[key].Type === "AWS::Lambda::Function"; + }); + lambdaFunctions.forEach(function(f) { + const fn = template.Resources[f]; + if (fn.Properties.Code.hasOwnProperty('S3Bucket')) { + // Set the S3 key reference + let artifactHash = Object.assign(fn.Properties.Code.S3Bucket.Ref); + artifactHash = artifactHash.replace(_regex, ''); + artifactHash = artifactHash.substring(0, artifactHash.indexOf('S3Bucket')); + const assetPath = `asset${artifactHash}`; + fn.Properties.Code.S3Key = `%%SOLUTION_NAME%%/%%VERSION%%/${assetPath}.zip`; + // Set the S3 bucket reference + fn.Properties.Code.S3Bucket = { + 'Fn::Sub': '%%BUCKET_NAME%%-${AWS::Region}' + }; + // Set the handler + const handler = fn.Properties.Handler; + fn.Properties.Handler = `${assetPath}/${handler}`; + } + }); + + // Clean-up Lambda Layer code dependencies + const lambdaLayers = Object.keys(resources).filter(function (key) { + return resources[key].Type === "AWS::Lambda::LayerVersion"; + }) + lambdaLayers.forEach(function (l) { + const layer = template.Resources[l]; + if (layer.Properties.Content.hasOwnProperty('S3Bucket')) { + let s3Key = Object.assign(layer.Properties.Content.S3Key); + layer.Properties.Content.S3Key = `%%SOLUTION_NAME%%/%%VERSION%%/${s3Key}`; + layer.Properties.Content.S3Bucket = { + 'Fn::Sub': '%%BUCKET_NAME%%-${AWS::Region}' + } + } + }) + + + // For the below code to work with nested templates, the nested template file has to + // be added as metadata to the construct in the CDK code. + // + // const myNestedTemplate = new MyNestedStack(this, 'NestedStack', { + // parameters: { + // "FirstParameter": , + // "SecondParameter": , + // "SolutionID": solutionID, + // "SolutionName": solutionName, + // "ParentStackName": Aws.STACK_NAME + // } + // }); + // + // The slice at the end of the statement is to remove the ".json" extension. Since all templates are published ".template" + // myNestedTemplate.nestedStackResource!.addMetadata('nestedStackFileName', myNestedTemplate.templateFile.slice(0, -(".json".length))); + // + // The below block of code has been tested to work with single nesting levels. This block of code has not been tested for multilevel + // nesting of stacks + // nestedStacks.forEach(function(f) { + // const fn = template.Resources[f]; + // fn.Properties.TemplateURL = { + // 'Fn::Join': [ + // '', + // [ + // 'https://%%TEMPLATE_BUCKET_NAME%%.s3.', + // { + // 'Ref' : 'AWS::URLSuffix' + // }, + // '/', + // `%%SOLUTION_NAME%%/%%VERSION%%/${fn.Metadata.nestedStackFileName}` + // ] + // ] + // }; + + // const params = fn.Properties.Parameters ? fn.Properties.Parameters : {}; + // const nestedStackParameters = Object.keys(params).filter(function(key) { + // if (key.search(_regex) > -1) { + // return true; + // } + // return false; + // }); + + // nestedStackParameters.forEach(function(stkParam) { + // fn.Properties.Parameters[stkParam] = undefined; + // }) + // }); + + // Clean-up parameters section + const parameters = (template.Parameters) ? template.Parameters : {}; + const assetParameters = Object.keys(parameters).filter(function(key) { + if (key.search(_regex) > -1) { + return true; + } + return false; + }); + assetParameters.forEach(function(a) { + template.Parameters[a] = undefined; + }); + + // Output modified template file + const output_template = JSON.stringify(template, null, 2); + fs.writeFileSync(`${global_s3_assets}/${file}`, output_template); +}); diff --git a/deployment/cdk-solution-helper/package.json b/deployment/cdk-solution-helper/package.json new file mode 100755 index 000000000..89fac67ae --- /dev/null +++ b/deployment/cdk-solution-helper/package.json @@ -0,0 +1,10 @@ +{ + "name": "cdk-solution-helper", + "version": "0.1.0", + "devDependencies": { + "fs": "0.0.1-security" + }, + "dependencies": { + "fs": "0.0.1-security" + } +} diff --git a/deployment/solution_config b/deployment/solution_config new file mode 100644 index 000000000..0b83b4905 --- /dev/null +++ b/deployment/solution_config @@ -0,0 +1,3 @@ +SOLUTION_ID='SO0999' +SOLUTION_NAME='AWS Compliance Accelerator' +SOLUTION_TRADEMARKEDNAME='aws-compliance-accelerator' \ No newline at end of file diff --git a/reference/sample-configurations/aws-best-practices/accounts-config.yaml b/reference/sample-configurations/aws-best-practices/accounts-config.yaml new file mode 100644 index 000000000..dba5dda38 --- /dev/null +++ b/reference/sample-configurations/aws-best-practices/accounts-config.yaml @@ -0,0 +1,22 @@ +mandatoryAccounts: + - name: Management + description: The management (primary) account. Do not change the name field for this mandatory account. + email: @example.com <----- UPDATE EMAIL ADDRESS + organizationalUnit: Root + - name: LogArchive + description: The log archive account. Do not change the name field for this mandatory account. + email: @example.com <----- UPDATE EMAIL ADDRESS + organizationalUnit: Security + - name: Audit + description: The security audit account (also referred to as the audit account). Do not change the name field for this mandatory account. + email: @example.com <----- UPDATE EMAIL ADDRESS + organizationalUnit: Security +workloadAccounts: + - name: SharedServices + description: The SharedServices account + email: @example.com <----- UPDATE EMAIL ADDRESS + organizationalUnit: Infrastructure + - name: Network + description: The Network account + email: @example.com <----- UPDATE EMAIL ADDRESS + organizationalUnit: Infrastructure diff --git a/reference/sample-configurations/aws-best-practices/backup-policies/org-backup-policies.json b/reference/sample-configurations/aws-best-practices/backup-policies/org-backup-policies.json new file mode 100644 index 000000000..aadece2e5 --- /dev/null +++ b/reference/sample-configurations/aws-best-practices/backup-policies/org-backup-policies.json @@ -0,0 +1,267 @@ +{ + "plans": { + "Organization_Backup_Plan": { + "rules": { + "Monthly_Rule": { + "schedule_expression": { + "@@assign": "cron(0 8 1 * ? *)" + }, + "start_backup_window_minutes": { + "@@assign": "60" + }, + "complete_backup_window_minutes": { + "@@assign": "604800" + }, + "enable_continuous_backup": { + "@@assign": false + }, + "target_backup_vault_name": { + "@@assign": "BackupVault" + }, + "recovery_point_tags": { + "Owner": { + "tag_key": { + "@@assign": "Owner" + }, + "tag_value": { + "@@assign": "Backup" + } + } + }, + "lifecycle": { + "move_to_cold_storage_after_days": { + "@@assign": "365" + }, + "delete_after_days": { + "@@assign": "1825" + } + } + }, + "Weekly_Rule": { + "schedule_expression": { + "@@assign": "cron(0 8 ? * MON *)" + }, + "start_backup_window_minutes": { + "@@assign": "60" + }, + "complete_backup_window_minutes": { + "@@assign": "604800" + }, + "enable_continuous_backup": { + "@@assign": false + }, + "target_backup_vault_name": { + "@@assign": "BackupVault" + }, + "recovery_point_tags": { + "Owner": { + "tag_key": { + "@@assign": "Owner" + }, + "tag_value": { + "@@assign": "Backup" + } + } + }, + "lifecycle": { + "move_to_cold_storage_after_days": { + "@@assign": "365" + }, + "delete_after_days": { + "@@assign": "1825" + } + } + }, + "Daily_Rule": { + "schedule_expression": { + "@@assign": "cron(15 10 * * ? *)" + }, + "start_backup_window_minutes": { + "@@assign": "60" + }, + "complete_backup_window_minutes": { + "@@assign": "604800" + }, + "enable_continuous_backup": { + "@@assign": false + }, + "target_backup_vault_name": { + "@@assign": "BackupVault" + }, + "recovery_point_tags": { + "Owner": { + "tag_key": { + "@@assign": "Owner" + }, + "tag_value": { + "@@assign": "Backup" + } + } + }, + "lifecycle": { + "move_to_cold_storage_after_days": { + "@@assign": "365" + }, + "delete_after_days": { + "@@assign": "1825" + } + } + }, + "NinetyDay_Rule": { + "schedule_expression": { + "@@assign": "cron(0 8 1 */3 ? *)" + }, + "start_backup_window_minutes": { + "@@assign": "60" + }, + "complete_backup_window_minutes": { + "@@assign": "604800" + }, + "enable_continuous_backup": { + "@@assign": false + }, + "target_backup_vault_name": { + "@@assign": "BackupVault" + }, + "recovery_point_tags": { + "Owner": { + "tag_key": { + "@@assign": "Owner" + }, + "tag_value": { + "@@assign": "Backup" + } + } + }, + "lifecycle": { + "move_to_cold_storage_after_days": { + "@@assign": "365" + }, + "delete_after_days": { + "@@assign": "1825" + } + } + }, + "Yearly_Rule": { + "schedule_expression": { + "@@assign": "cron(0 8 1 */12 ? *)" + }, + "start_backup_window_minutes": { + "@@assign": "60" + }, + "complete_backup_window_minutes": { + "@@assign": "604800" + }, + "enable_continuous_backup": { + "@@assign": false + }, + "target_backup_vault_name": { + "@@assign": "BackupVault" + }, + "recovery_point_tags": { + "Owner": { + "tag_key": { + "@@assign": "Owner" + }, + "tag_value": { + "@@assign": "Backup" + } + } + }, + "lifecycle": { + "move_to_cold_storage_after_days": { + "@@assign": "1825" + }, + "delete_after_days": { + "@@assign": "365" + } + } + } + }, + "regions": { + "@@append": [ + "us-east-1" + ] + }, + "selections": { + "tags": { + "Monthly_Backup_Assignment": { + "iam_role_arn": { + "@@assign": "arn:aws:iam::$account:role/Backup-Role" + }, + "tag_key": { + "@@assign": "orgBackupMonthly" + }, + "tag_value": { + "@@assign": [ + "Monthly" + ] + } + }, + "Weekly_Backup_Assignment": { + "iam_role_arn": { + "@@assign": "arn:aws:iam::$account:role/Backup-Role" + }, + "tag_key": { + "@@assign": "orgBackupWeekly" + }, + "tag_value": { + "@@assign": [ + "Weekly" + ] + } + }, + "Daily_Backup_Assignment": { + "iam_role_arn": { + "@@assign": "arn:aws:iam::$account:role/Backup-Role" + }, + "tag_key": { + "@@assign": "orgBackupDaily" + }, + "tag_value": { + "@@assign": [ + "Daily" + ] + } + }, + "NinetyDay_Backup_Assignment": { + "iam_role_arn": { + "@@assign": "arn:aws:iam::$account:role/Backup-Role" + }, + "tag_key": { + "@@assign": "orgBackupNinetyDay" + }, + "tag_value": { + "@@assign": [ + "NinetyDay" + ] + } + }, + "Yearly_Backup_Assignment": { + "iam_role_arn": { + "@@assign": "arn:aws:iam::$account:role/Backup-Role" + }, + "tag_key": { + "@@assign": "orgBackupYearly" + }, + "tag_value": { + "@@assign": [ + "Yearly" + ] + } + } + } + }, + "backup_plan_tags": { + "stage": { + "tag_key": { + "@@assign": "Stage" + }, + "tag_value": { + "@@assign": "Released" + } + } + } + } + } +} \ No newline at end of file diff --git a/reference/sample-configurations/aws-best-practices/custom-config-rules/attach-ec2-instance-profile-detection-role.json b/reference/sample-configurations/aws-best-practices/custom-config-rules/attach-ec2-instance-profile-detection-role.json new file mode 100644 index 000000000..db821df5b --- /dev/null +++ b/reference/sample-configurations/aws-best-practices/custom-config-rules/attach-ec2-instance-profile-detection-role.json @@ -0,0 +1,16 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "ec2:Describe*", + "ec2:Get*", + "ec2:ListSnapshotsInRecycleBin", + "ec2:SearchLocalGatewayRoutes", + "ec2:SearchTransitGatewayRoutes" + ], + "Resource": "*" + } + ] +} \ No newline at end of file diff --git a/reference/sample-configurations/aws-best-practices/custom-config-rules/attach-ec2-instance-profile-remediation-role.json b/reference/sample-configurations/aws-best-practices/custom-config-rules/attach-ec2-instance-profile-remediation-role.json new file mode 100644 index 000000000..52701c496 --- /dev/null +++ b/reference/sample-configurations/aws-best-practices/custom-config-rules/attach-ec2-instance-profile-remediation-role.json @@ -0,0 +1,14 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "ec2:AssociateIamInstanceProfile", + "ec2:ReplaceIamInstanceProfileAssociation", + "iam:PassRole" + ], + "Resource": "*" + } + ] +} \ No newline at end of file diff --git a/reference/sample-configurations/aws-best-practices/custom-config-rules/attach-ec2-instance-profile.zip b/reference/sample-configurations/aws-best-practices/custom-config-rules/attach-ec2-instance-profile.zip new file mode 100644 index 0000000000000000000000000000000000000000..1de00bf06d02573ed8890d48da07d3c50ed785dc GIT binary patch literal 987 zcmWIWW@Zs#-~hs}B^f~sQ1F+70VtxtkeQc~TA`O!92&yQz<&LuYr;(+2GOMz+zgB? zq7NO*Sk6>47j6wZm?vc@u=lg5@XAw7F6KdfA(jD$8nqfZR#bH?dQic>VN-T~u0D(F zpX%e;(^4A+LblF1V4R}~FRwR-!${OMK~i1d4x^c&R)1FH?mAy5u>2*L=IrOQWFLCFF_mpk zTpd;MxlyY1^FEb*3(hJmpQ(Fzmang~No3CPSE8xhdmMJzWIeO9ULj`l%zZ=Vir-Ug zG-sSLIUaY#O6B#VyftTqBh;U2ZrLn3%Yq{>7<#t{oL2L{{QrZl|Oi`P~s=;Bwwr0?Z0pQ5l}FFIxo7y-e_C) ztOIv8-sta67UzE}-@lo|_istR^wqgHU!DDKCHIB>@pGfVfPggXy)%2BbKHt`o9Lv> z%&p`5STusw)r&cc>)P6n)4yMCU8`<))G9vtz>S1+-*^PwLjuzNxCWlNt$QrPkn5k~ z-2)fbs<1y~i85lI=$gq(BwSQb`SYegdz0$Xv-@{` zIluId1^@*MvS$DQ literal 0 HcmV?d00001 diff --git a/reference/sample-configurations/aws-best-practices/custom-config-rules/bucket-sse-enabled-remediation-role.json b/reference/sample-configurations/aws-best-practices/custom-config-rules/bucket-sse-enabled-remediation-role.json new file mode 100644 index 000000000..3c37861da --- /dev/null +++ b/reference/sample-configurations/aws-best-practices/custom-config-rules/bucket-sse-enabled-remediation-role.json @@ -0,0 +1,15 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": ["s3:GetEncryptionConfiguration", "s3:PutEncryptionConfiguration"], + "Resource": "*", + "Condition": { + "ArnLike": { + "aws:PrincipalArn": ["arn:aws:iam::*:role/AWSAccelerator-SecuritySt-*"] + } + } + } + ] +} diff --git a/reference/sample-configurations/aws-best-practices/custom-config-rules/ec2-instance-profile-permissions-detection-role.json b/reference/sample-configurations/aws-best-practices/custom-config-rules/ec2-instance-profile-permissions-detection-role.json new file mode 100644 index 000000000..e612dc87f --- /dev/null +++ b/reference/sample-configurations/aws-best-practices/custom-config-rules/ec2-instance-profile-permissions-detection-role.json @@ -0,0 +1,13 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "iam:Get*", + "iam:List*" + ], + "Resource": "*" + } + ] +} diff --git a/reference/sample-configurations/aws-best-practices/custom-config-rules/ec2-instance-profile-permissions-remediation-role.json b/reference/sample-configurations/aws-best-practices/custom-config-rules/ec2-instance-profile-permissions-remediation-role.json new file mode 100644 index 000000000..83c5a8daa --- /dev/null +++ b/reference/sample-configurations/aws-best-practices/custom-config-rules/ec2-instance-profile-permissions-remediation-role.json @@ -0,0 +1,13 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "config:BatchGetResourceConfig", + "iam:AttachRolePolicy" + ], + "Resource": "*" + } + ] +} diff --git a/reference/sample-configurations/aws-best-practices/custom-config-rules/ec2-instance-profile-permissions.zip b/reference/sample-configurations/aws-best-practices/custom-config-rules/ec2-instance-profile-permissions.zip new file mode 100644 index 0000000000000000000000000000000000000000..9d64fe687967e4fa3cf01897db666d68356cb1af GIT binary patch literal 1292 zcmWIWW@Zs#-~hr?Ejd99P~gSS02EPR$jnPgtcVx~UR-qK+7Txoqf#cY`$ghLj~zm}!V^8GE_ovn z`S&}Yr>*YMNS2oi_uiay{QKF%#*aM;mL~qWF0{_!(tcyLmG37u|B5`?A6H=M;3>hm zFKxDg;B4NhE86c*^?30#K_D_)-JNkc`(v#+H`-OFR*MDNuMj!Def#yzCx3i@{qpbK zlAS+3=NXyqm~U$$)tmNs_Qe-`veoeyn(UT3Wr^l-_6u`+UzzB>;atU%nGYUZwtZTa zaly3q5~B_Of+cA?e(_KC$=ukyTW8@0F`-4ey>gQG53KcYIkJLjk?Fzf5?dHr*gLmP z(5yL8(X+rf#ka!cVOW=`^Zvvi4G*^z{)^Ciw(ydW?dGo=mmJ-!B6(*$FlJ zJdG;@4|c3c^;)v%oK}uzh{HJpCABwK4VPxz4B{yd(cHAqE$p@j&+lm-n}TaSrcTMy z60P~ocCYwdq{lY4|LwP|jy$>}(DyI-jP&Np-yaxfN$Fh-nYUu+B!(AjMc8l41Z%c_ z*{gXu`pi4m>z`Q@=IX_LC}(uJw8=YJ-{E=X!S5!H;o%Q-eN!i<*oe$yj~GN&RZ^+BwtZ_wV4Z3=r|`k_|3f z@hsx_9EFew%~`ypDUAoI!}zSW@=FD zx|Oki@h)$bDVpXfr_%no2+vuv@q6FHDz!Ue76!(X`CJ^|JP5FNo2t{v`1XM|YYu~$ z@Zlwr_ZDb8)joK0=f5sKv3qZiB&o(3FTHP_vAOH4B7@J(9e0e>{@jQ>ac_24#-keN z8Hr9m-STrE*8g08H0G0T*;V0V&B4SvPJtDi!u)yMj>Ar4xp(^M2X~c(Ze?O!&FVi-my!R0;)nGct)VBT6V_IZz1& d14|k~EMm*60B=?{ka|WS3 + Accelerator GuardRails 1 + policy: service-control-policies/guardrails-1.json + type: customerManaged + deploymentTargets: + organizationalUnits: + - Infrastructure + - name: AcceleratorGuardrails2 + description: > + Accelerator GuardRails 2 + policy: service-control-policies/guardrails-2.json + type: customerManaged + deploymentTargets: + organizationalUnits: + - Infrastructure + - name: Quarantine + description: > + This SCP is used to prevent changes to new accounts until the Accelerator + has been executed successfully. + This policy will be applied upon account creation if enabled. + policy: service-control-policies/quarantine.json + type: customerManaged + deploymentTargets: + organizationalUnits: [] +taggingPolicies: + - name: TagPolicy + description: Organization Tagging Policy + policy: tagging-policies/org-tag-policy.json + deploymentTargets: + organizationalUnits: + - Root +backupPolicies: + - name: BackupPolicy + description: Organization Backup Policy + policy: backup-policies/org-backup-policies.json + deploymentTargets: + organizationalUnits: + - Root diff --git a/reference/sample-configurations/aws-best-practices/security-config.yaml b/reference/sample-configurations/aws-best-practices/security-config.yaml new file mode 100644 index 000000000..a78e38d93 --- /dev/null +++ b/reference/sample-configurations/aws-best-practices/security-config.yaml @@ -0,0 +1,656 @@ +homeRegion: &HOME_REGION us-east-1 +centralSecurityServices: + delegatedAdminAccount: Audit + ebsDefaultVolumeEncryption: + enable: true + excludeRegions: [] + s3PublicAccessBlock: + enable: true + excludeAccounts: [] + snsSubscriptions: + - level: High + email: @example.com <----- UPDATE EMAIL ADDRESS + - level: Medium + email: @example.com <----- UPDATE EMAIL ADDRESS + - level: Low + email: @example.com <----- UPDATE EMAIL ADDRESS + macie: + enable: true + excludeRegions: [] + policyFindingsPublishingFrequency: FIFTEEN_MINUTES + publishSensitiveDataFindings: true + guardduty: + enable: true + excludeRegions: [] + s3Protection: + enable: true + excludeRegions: [] + exportConfiguration: + enable: true + destinationType: S3 + exportFrequency: FIFTEEN_MINUTES + securityHub: + enable: true + regionAggregation: true + excludeRegions: [] + standards: + - name: AWS Foundational Security Best Practices v1.0.0 + enable: true + controlsToDisable: + - IAM.1 + - EC2.10 + - Lambda.4 + - name: PCI DSS v3.2.1 + enable: true + controlsToDisable: + - PCI.IAM.3 + - PCI.S3.3 + - PCI.EC2.3 + - PCI.Lambda.2 + - name: CIS AWS Foundations Benchmark v1.2.0 + enable: true + controlsToDisable: + - CIS.1.20 + - CIS.1.22 + - CIS.2.6 + ssmAutomation: + documentSets: + - shareTargets: + organizationalUnits: + - Root + documents: + # Calls the AWS CLI to enable access logs on a specified ELB + - name: SSM-ELB-Enable-Logging + template: ssm-documents/ssm-elb-enable-logging.yaml + # Enables S3 encryption using KMS + - name: Put-S3-Encryption + template: ssm-documents/s3-encryption.yaml + # Attaches instance profiles to an EC2 instance + - name: Attach-IAM-Instance-Profile + template: ssm-documents/attach-iam-instance-profile.yaml + # Attaches Aws IAM Managed Policy to IAM Role + - name: Attach-IAM-Role-Policy + template: ssm-documents/attach-iam-role-policy.yaml +accessAnalyzer: + enable: true +iamPasswordPolicy: + allowUsersToChangePassword: true + hardExpiry: false + requireUppercaseCharacters: true + requireLowercaseCharacters: true + requireSymbols: true + requireNumbers: true + minimumPasswordLength: 14 + passwordReusePrevention: 24 + maxPasswordAge: 90 +awsConfig: + enableConfigurationRecorder: true + enableDeliveryChannel: true + ruleSets: + - deploymentTargets: + organizationalUnits: + - Root + rules: + - name: accelerator-attach-ec2-instance-profile + type: Custom + description: Custom rule for checking EC2 instance IAM profile attachment + inputParameters: + customRule: + lambda: + sourceFilePath: custom-config-rules/attach-ec2-instance-profile.zip + handler: index.handler + runtime: nodejs14.x + rolePolicyFile: custom-config-rules/attach-ec2-instance-profile-detection-role.json + periodic: true + maximumExecutionFrequency: Six_Hours + configurationChanges: true + triggeringResources: + lookupType: ResourceTypes + lookupKey: ResourceTypes + lookupValue: + - AWS::EC2::Instance + remediation: + rolePolicyFile: custom-config-rules/attach-ec2-instance-profile-remediation-role.json + automatic: true + targetId: Attach-IAM-Instance-Profile + retryAttemptSeconds: 60 + maximumAutomaticAttempts: 5 + parameters: + - name: InstanceId + value: RESOURCE_ID + type: String + - name: IamInstanceProfile + value: ${ACCEL_LOOKUP::InstanceProfile:EC2-Default-SSM-AD-Role} + type: StringList + - name: accelerator-ec2-instance-profile-permission + type: Custom + description: Custom role to remediate EC2 instance profile permission + inputParameters: + AWSManagedPolicies: AmazonSSMManagedInstanceCore,AmazonSSMDirectoryServiceAccess,CloudWatchAgentServerPolicy + # CustomerManagedPolicies: ${ACCEL_LOOKUP::CustomerManagedPolicy:},${ACCEL_LOOKUP::CustomerManagedPolicy:} + ResourceId: RESOURCE_ID + customRule: + lambda: + sourceFilePath: custom-config-rules/ec2-instance-profile-permissions.zip + handler: index.handler + runtime: nodejs14.x + rolePolicyFile: custom-config-rules/ec2-instance-profile-permissions-detection-role.json + periodic: true + maximumExecutionFrequency: Six_Hours + configurationChanges: true + triggeringResources: + lookupType: ResourceTypes + lookupKey: ResourceTypes + lookupValue: + - AWS::IAM::Role + remediation: + rolePolicyFile: custom-config-rules/ec2-instance-profile-permissions-remediation-role.json + automatic: true + targetId: Attach-IAM-Role-Policy + targetAccountName: Audit + retryAttemptSeconds: 60 + maximumAutomaticAttempts: 5 + parameters: + - name: ResourceId + value: RESOURCE_ID + type: String + - name: AWSManagedPolicies + value: AmazonSSMManagedInstanceCore,AmazonSSMDirectoryServiceAccess,CloudWatchAgentServerPolicy + type: StringList + # - name: CustomerManagedPolicies + # value: ${ACCEL_LOOKUP::CustomerManagedPolicy:policy-00},${ACCEL_LOOKUP::CustomerManagedPolicy:policy-01} + # type: StringList + - name: accelerator-s3-bucket-server-side-encryption-enabled + identifier: S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED + complianceResourceTypes: + - AWS::S3::Bucket + remediation: + rolePolicyFile: custom-config-rules/bucket-sse-enabled-remediation-role.json + automatic: true + targetId: Put-S3-Encryption + retryAttemptSeconds: 60 + maximumAutomaticAttempts: 5 + parameters: + - name: BucketName + value: RESOURCE_ID + type: String + - name: KMSMasterKey + value: ${ACCEL_LOOKUP::KMS} + type: StringList + - name: accelerator-elb-logging-enabled + identifier: ELB_LOGGING_ENABLED + complianceResourceTypes: + - AWS::ElasticLoadBalancing::LoadBalancer + - AWS::ElasticLoadBalancingV2::LoadBalancer + inputParameters: + s3BucketNames: ${ACCEL_LOOKUP::Bucket:elbLogs} + remediation: + rolePolicyFile: custom-config-rules/elb-logging-enabled-remediation-role.json + automatic: true + targetId: SSM-ELB-Enable-Logging + retryAttemptSeconds: 60 + maximumAutomaticAttempts: 5 + parameters: + - name: LoadBalancerArn + value: RESOURCE_ID + type: String + - name: LogDestination + value: ${ACCEL_LOOKUP::Bucket:elbLogs} + type: StringList + - name: accelerator-iam-user-group-membership-check + complianceResourceTypes: + - AWS::IAM::User + identifier: IAM_USER_GROUP_MEMBERSHIP_CHECK + - name: accelerator-securityhub-enabled + identifier: SECURITYHUB_ENABLED + - name: accelerator-cloudtrail-enabled + identifier: CLOUD_TRAIL_ENABLED + - name: accelerator-rds-logging-enabled + complianceResourceTypes: + - AWS::RDS::DBInstance + identifier: RDS_LOGGING_ENABLED + - name: accelerator-cloudwatch-alarm-action-check + complianceResourceTypes: + - AWS::CloudWatch::Alarm + inputParameters: + alarmActionRequired: "TRUE" + insufficientDataActionRequired: "TRUE" + okActionRequired: "FALSE" + identifier: CLOUDWATCH_ALARM_ACTION_CHECK + - name: accelerator-redshift-cluster-configuration-check + inputParameters: + clusterDbEncrypted: "TRUE" + loggingEnabled: "TRUE" + complianceResourceTypes: + - AWS::Redshift::Cluster + identifier: REDSHIFT_CLUSTER_CONFIGURATION_CHECK + - name: accelerator-cloudtrail-s3-dataevents-enabled + identifier: CLOUDTRAIL_S3_DATAEVENTS_ENABLED + - name: accelerator-emr-kerberos-enabled + identifier: EMR_KERBEROS_ENABLED + - name: accelerator-iam-group-has-users-check + complianceResourceTypes: + - AWS::IAM::Group + identifier: IAM_GROUP_HAS_USERS_CHECK + - name: accelerator-s3-bucket-policy-grantee-check + complianceResourceTypes: + - AWS::S3::Bucket + identifier: S3_BUCKET_POLICY_GRANTEE_CHECK + - name: accelerator-lambda-inside-vpc + complianceResourceTypes: + - AWS::Lambda::Function + identifier: LAMBDA_INSIDE_VPC + - name: accelerator-ec2-instances-in-vpc + complianceResourceTypes: + - AWS::EC2::Instance + identifier: INSTANCES_IN_VPC + - name: accelerator-vpc-sg-open-only-to-authorized-ports + inputParameters: + authorizedTcpPorts: "443" + authorizedUdpPorts: "1020-1025" + complianceResourceTypes: + - AWS::EC2::SecurityGroup + identifier: VPC_SG_OPEN_ONLY_TO_AUTHORIZED_PORTS + - name: accelerator-ec2-instance-no-public-ip + complianceResourceTypes: + - AWS::EC2::Instance + identifier: EC2_INSTANCE_NO_PUBLIC_IP + - name: accelerator-elasticsearch-in-vpc-only + identifier: ELASTICSEARCH_IN_VPC_ONLY + - name: accelerator-internet-gateway-authorized-vpc-only + complianceResourceTypes: + - AWS::EC2::InternetGateway + identifier: INTERNET_GATEWAY_AUTHORIZED_VPC_ONLY + - name: accelerator-iam-no-inline-policy-check + complianceResourceTypes: + - AWS::IAM::User + - AWS::IAM::Role + - AWS::IAM::Group + identifier: IAM_NO_INLINE_POLICY_CHECK + - name: accelerator-elb-acm-certificate-required + complianceResourceTypes: + - AWS::ElasticLoadBalancing::LoadBalancer + identifier: ELB_ACM_CERTIFICATE_REQUIRED + - name: accelerator-alb-http-drop-invalid-header-enabled + complianceResourceTypes: + - AWS::ElasticLoadBalancingV2::LoadBalancer + identifier: ALB_HTTP_DROP_INVALID_HEADER_ENABLED + - name: accelerator-elb-tls-https-listeners-only + complianceResourceTypes: + - AWS::ElasticLoadBalancing::LoadBalancer + identifier: ELB_TLS_HTTPS_LISTENERS_ONLY + - name: accelerator-api-gw-execution-logging-enabled + complianceResourceTypes: + - AWS::ApiGateway::Stage + - AWS::ApiGatewayV2::Stage + identifier: API_GW_EXECUTION_LOGGING_ENABLED + - name: accelerator-cloudwatch-log-group-encrypted + identifier: CLOUDWATCH_LOG_GROUP_ENCRYPTED + - name: accelerator-s3-bucket-replication-enabled + complianceResourceTypes: + - AWS::S3::Bucket + identifier: S3_BUCKET_REPLICATION_ENABLED + - name: accelerator-cw-loggroup-retention-period-check + identifier: CW_LOGGROUP_RETENTION_PERIOD_CHECK + - name: accelerator-ec2-instance-detailed-monitoring-enabled + complianceResourceTypes: + - AWS::EC2::Instance + identifier: EC2_INSTANCE_DETAILED_MONITORING_ENABLED + - name: accelerator-ec2-volume-inuse-check + inputParameters: + deleteOnTermination: "TRUE" + complianceResourceTypes: + - AWS::EC2::Volume + identifier: EC2_VOLUME_INUSE_CHECK + - name: accelerator-elb-deletion-protection-enabled + complianceResourceTypes: + - AWS::ElasticLoadBalancingV2::LoadBalancer + identifier: ELB_DELETION_PROTECTION_ENABLED + - name: accelerator-cloudtrail-security-trail-enabled + identifier: CLOUDTRAIL_SECURITY_TRAIL_ENABLED + - name: accelerator-elasticache-redis-cluster-automatic-backup-check + identifier: ELASTICACHE_REDIS_CLUSTER_AUTOMATIC_BACKUP_CHECK + - name: accelerator-s3-bucket-versioning-enabled + complianceResourceTypes: + - AWS::S3::Bucket + identifier: S3_BUCKET_VERSIONING_ENABLED + - name: accelerator-vpc-vpn-2-tunnels-up + complianceResourceTypes: + - AWS::EC2::VPNConnection + identifier: VPC_VPN_2_TUNNELS_UP + - name: accelerator-elb-cross-zone-load-balancing-enabled + complianceResourceTypes: + - AWS::ElasticLoadBalancing::LoadBalancer + identifier: ELB_CROSS_ZONE_LOAD_BALANCING_ENABLED + - name: accelerator-iam-user-mfa-enabled + identifier: IAM_USER_MFA_ENABLED + - name: accelerator-guardduty-non-archived-findings + inputParameters: + daysHighSev: "1" + daysLowSev: "30" + daysMediumSev: "7" + identifier: GUARDDUTY_NON_ARCHIVED_FINDINGS + - name: accelerator-elasticsearch-node-to-node-encryption-check + complianceResourceTypes: + - AWS::Elasticsearch::Domain + identifier: ELASTICSEARCH_NODE_TO_NODE_ENCRYPTION_CHECK + - name: accelerator-kms-cmk-not-scheduled-for-deletion + complianceResourceTypes: + - AWS::KMS::Key + identifier: KMS_CMK_NOT_SCHEDULED_FOR_DELETION + - name: accelerator-api-gw-cache-enabled-and-encrypted + complianceResourceTypes: + - AWS::ApiGateway::Stage + identifier: API_GW_CACHE_ENABLED_AND_ENCRYPTED + - name: accelerator-sagemaker-endpoint-configuration-kms-key-configured + identifier: SAGEMAKER_ENDPOINT_CONFIGURATION_KMS_KEY_CONFIGURED + - name: accelerator-sagemaker-notebook-instance-kms-key-configured + identifier: SAGEMAKER_NOTEBOOK_INSTANCE_KMS_KEY_CONFIGURED + - name: accelerator-dynamodb-table-encrypted-kms + complianceResourceTypes: + - AWS::DynamoDB::Table + identifier: DYNAMODB_TABLE_ENCRYPTED_KMS + - name: accelerator-s3-bucket-default-lock-enabled + complianceResourceTypes: + - AWS::S3::Bucket + identifier: S3_BUCKET_DEFAULT_LOCK_ENABLED +cloudWatch: + metricSets: + - regions: + - *HOME_REGION + deploymentTargets: + organizationalUnits: + - Root + metrics: + # CIS 1.1 – Avoid the use of the "root" account + - filterName: RootAccountMetricFilter + logGroupName: aws-controltower/CloudTrailLogs + filterPattern: '{$.userIdentity.type="Root" && $.userIdentity.invokedBy NOT EXISTS && $.eventType !="AwsServiceEvent"}' + metricNamespace: LogMetrics + metricName: RootAccount + metricValue: "1" + # CIS 3.1 – Ensure a log metric filter and alarm exist for unauthorized API calls + - filterName: UnauthorizedAPICallsMetricFilter + logGroupName: aws-controltower/CloudTrailLogs + filterPattern: '{($.errorCode="*UnauthorizedOperation") || ($.errorCode="AccessDenied*")}' + metricNamespace: LogMetrics + metricName: UnauthorizedAPICalls + metricValue: "1" + # CIS 3.2 – Ensure a log metric filter and alarm exist for AWS Management Console sign-in without MFA + - filterName: ConsoleSigninWithoutMFAMetricFilter + logGroupName: aws-controltower/CloudTrailLogs + filterPattern: '{($.eventName="ConsoleLogin") && ($.additionalEventData.MFAUsed !="Yes")}' + metricNamespace: LogMetrics + metricName: ConsoleSigninWithoutMFA + metricValue: "1" + # CIS 3.3 – Ensure a log metric filter and alarm exist for usage of "root" account + - filterName: MetricFilter + logGroupName: aws-controltower/CloudTrailLogs + filterPattern: '{$.userIdentity.type="Root" && $.userIdentity.invokedBy NOT EXISTS && $.eventType !="AwsServiceEvent"}' + metricNamespace: LogMetrics + metricName: RootAccountUsage + metricValue: "1" + # CIS 3.4 – Ensure a log metric filter and alarm exist for IAM policy changes + - filterName: IAMPolicyChangesMetricFilter + logGroupName: aws-controltower/CloudTrailLogs + filterPattern: "{($.eventName=DeleteGroupPolicy) || ($.eventName=DeleteRolePolicy) || ($.eventName=DeleteUserPolicy) || ($.eventName=PutGroupPolicy) || ($.eventName=PutRolePolicy) || ($.eventName=PutUserPolicy) || ($.eventName=CreatePolicy) || ($.eventName=DeletePolicy) || ($.eventName=CreatePolicyVersion) || ($.eventName=DeletePolicyVersion) || ($.eventName=AttachRolePolicy) || ($.eventName=DetachRolePolicy) || ($.eventName=AttachUserPolicy) || ($.eventName=DetachUserPolicy) || ($.eventName=AttachGroupPolicy) || ($.eventName=DetachGroupPolicy)}" + metricNamespace: LogMetrics + metricName: IAMPolicyChanges + metricValue: "1" + # CIS 3.5 – Ensure a log metric filter and alarm exist for CloudTrail configuration changes + - filterName: CloudTrailChangesMetricFilter + logGroupName: aws-controltower/CloudTrailLogs + filterPattern: "{($.eventName=CreateTrail) || ($.eventName=UpdateTrail) || ($.eventName=DeleteTrail) || ($.eventName=StartLogging) || ($.eventName=StopLogging)}" + metricNamespace: LogMetrics + metricName: CloudTrailChanges + metricValue: "1" + # CIS 3.6 – Ensure a log metric filter and alarm exist for AWS Management Console authentication failures + - filterName: ConsoleAuthenticationFailureMetricFilter + logGroupName: aws-controltower/CloudTrailLogs + filterPattern: '{($.eventName=ConsoleLogin) && ($.errorMessage="Failed authentication")}' + metricNamespace: LogMetrics + metricName: ConsoleAuthenticationFailure + metricValue: "1" + # CIS 3.7 – Ensure a log metric filter and alarm exist for disabling or scheduled deletion of customer created CMKs + - filterName: DisableOrDeleteCMKMetricFilter + logGroupName: aws-controltower/CloudTrailLogs + filterPattern: "{($.eventSource=kms.amazonaws.com) && (($.eventName=DisableKey) || ($.eventName=ScheduleKeyDeletion))}" + metricNamespace: LogMetrics + metricName: DisableOrDeleteCMK + metricValue: "1" + # CIS 3.8 – Ensure a log metric filter and alarm exist for S3 bucket policy changes + - filterName: S3BucketPolicyChangesMetricFilter + logGroupName: aws-controltower/CloudTrailLogs + filterPattern: "{($.eventSource=s3.amazonaws.com) && (($.eventName=PutBucketAcl) || ($.eventName=PutBucketPolicy) || ($.eventName=PutBucketCors) || ($.eventName=PutBucketLifecycle) || ($.eventName=PutBucketReplication) || ($.eventName=DeleteBucketPolicy) || ($.eventName=DeleteBucketCors) || ($.eventName=DeleteBucketLifecycle) || ($.eventName=DeleteBucketReplication))}" + metricNamespace: LogMetrics + metricName: S3BucketPolicyChanges + metricValue: "1" + # CIS 3.9 – Ensure a log metric filter and alarm exist for AWS Config configuration changes + - filterName: AWSConfigChangesMetricFilter + logGroupName: aws-controltower/CloudTrailLogs + filterPattern: "{($.eventSource=config.amazonaws.com) && (($.eventName=StopConfigurationRecorder) || ($.eventName=DeleteDeliveryChannel) || ($.eventName=PutDeliveryChannel) || ($.eventName=PutConfigurationRecorder))}" + metricNamespace: LogMetrics + metricName: AWSConfigChanges + metricValue: "1" + # CIS 3.10 – Ensure a log metric filter and alarm exist for security group changes + - filterName: SecurityGroupChangesMetricFilter + logGroupName: aws-controltower/CloudTrailLogs + filterPattern: "{($.eventName=AuthorizeSecurityGroupIngress) || ($.eventName=AuthorizeSecurityGroupEgress) || ($.eventName=RevokeSecurityGroupIngress) || ($.eventName=RevokeSecurityGroupEgress) || ($.eventName=CreateSecurityGroup) || ($.eventName=DeleteSecurityGroup)}" + metricNamespace: LogMetrics + metricName: SecurityGroupChanges + metricValue: "1" + # CIS 3.11 – Ensure a log metric filter and alarm exist for changes to Network Access Control Lists (NACL) + - filterName: NetworkACLChangesMetricFilter + logGroupName: aws-controltower/CloudTrailLogs + filterPattern: "{($.eventName=CreateNetworkAcl) || ($.eventName=CreateNetworkAclEntry) || ($.eventName=DeleteNetworkAcl) || ($.eventName=DeleteNetworkAclEntry) || ($.eventName=ReplaceNetworkAclEntry) || ($.eventName=ReplaceNetworkAclAssociation)}" + metricNamespace: LogMetrics + metricName: NetworkACLChanges + metricValue: "1" + # CIS 3.12 – Ensure a log metric filter and alarm exist for changes to network gateways + - filterName: NetworkGatewayChangesMetricFilter + logGroupName: aws-controltower/CloudTrailLogs + filterPattern: "{($.eventName=CreateCustomerGateway) || ($.eventName=DeleteCustomerGateway) || ($.eventName=AttachInternetGateway) || ($.eventName=CreateInternetGateway) || ($.eventName=DeleteInternetGateway) || ($.eventName=DetachInternetGateway)}" + metricNamespace: LogMetrics + metricName: NetworkGatewayChanges + metricValue: "1" + # CIS 3.13 – Ensure a log metric filter and alarm exist for route table changes + - filterName: RouteTableChangesMetricFilter + logGroupName: aws-controltower/CloudTrailLogs + filterPattern: "{($.eventName=CreateRoute) || ($.eventName=CreateRouteTable) || ($.eventName=ReplaceRoute) || ($.eventName=ReplaceRouteTableAssociation) || ($.eventName=DeleteRouteTable) || ($.eventName=DeleteRoute) || ($.eventName=DisassociateRouteTable)}" + metricNamespace: LogMetrics + metricName: RouteTableChanges + metricValue: "1" + # CIS 3.14 – Ensure a log metric filter and alarm exist for VPC changes + - filterName: VPCChangesMetricFilter + logGroupName: aws-controltower/CloudTrailLogs + filterPattern: "{($.eventName=CreateVpc) || ($.eventName=DeleteVpc) || ($.eventName=ModifyVpcAttribute) || ($.eventName=AcceptVpcPeeringConnection) || ($.eventName=CreateVpcPeeringConnection) || ($.eventName=DeleteVpcPeeringConnection) || ($.eventName=RejectVpcPeeringConnection) || ($.eventName=AttachClassicLinkVpc) || ($.eventName=DetachClassicLinkVpc) || ($.eventName=DisableVpcClassicLink) || ($.eventName=EnableVpcClassicLink)}" + metricNamespace: LogMetrics + metricName: VPCChanges + metricValue: "1" + alarmSets: + - regions: + - *HOME_REGION + deploymentTargets: + organizationalUnits: + - Root + alarms: + # CIS 1.1 – Avoid the use of the "root" account + - alarmName: CIS-1.1-RootAccountUsage + alarmDescription: Alarm for usage of "root" account + snsAlertLevel: Low + metricName: RootAccountUsage + namespace: LogMetrics + comparisonOperator: GreaterThanOrEqualToThreshold + evaluationPeriods: 1 + period: 300 + statistic: Sum + threshold: 1 + treatMissingData: notBreaching + # CIS 3.1 – Ensure a log metric filter and alarm exist for unauthorized API calls + - alarmName: CIS-3.1-UnauthorizedAPICalls + alarmDescription: Alarm for unauthorized API calls + snsAlertLevel: Low + metricName: UnauthorizedAPICalls + namespace: LogMetrics + comparisonOperator: GreaterThanOrEqualToThreshold + evaluationPeriods: 1 + period: 300 + statistic: Average + threshold: 5 + treatMissingData: notBreaching + # CIS 3.2 – Ensure a log metric filter and alarm exist for AWS Management Console sign-in without MFA + - alarmName: CIS-3.2-ConsoleSigninWithoutMFA + alarmDescription: Alarm for AWS Management Console sign-in without MFA + snsAlertLevel: Low + metricName: ConsoleSigninWithoutMFA + namespace: LogMetrics + comparisonOperator: GreaterThanOrEqualToThreshold + evaluationPeriods: 1 + period: 300 + statistic: Sum + threshold: 1 + treatMissingData: notBreaching + # CIS 3.3 – Ensure a log metric filter and alarm exist for usage of "root" account + - alarmName: CIS-3.3-RootAccountUsage + alarmDescription: Alarm for usage of "root" account + snsAlertLevel: Low + metricName: RootAccountUsage + namespace: LogMetrics + comparisonOperator: GreaterThanOrEqualToThreshold + evaluationPeriods: 1 + period: 300 + statistic: Sum + threshold: 1 + treatMissingData: notBreaching + # CIS 3.4 – Ensure a log metric filter and alarm exist for IAM policy changes + - alarmName: CIS-3.4-IAMPolicyChanges + alarmDescription: Alarm for IAM policy changes + snsAlertLevel: Low + metricName: IAMPolicyChanges + namespace: LogMetrics + comparisonOperator: GreaterThanOrEqualToThreshold + evaluationPeriods: 1 + period: 300 + statistic: Average + threshold: 1 + treatMissingData: notBreaching + # CIS 3.5 – Ensure a log metric filter and alarm exist for CloudTrail configuration changes + - alarmName: CIS-3.5-CloudTrailChanges + alarmDescription: Alarm for CloudTrail configuration changes + snsAlertLevel: Low + metricName: CloudTrailChanges + namespace: LogMetrics + comparisonOperator: GreaterThanOrEqualToThreshold + evaluationPeriods: 1 + period: 300 + statistic: Sum + threshold: 1 + treatMissingData: notBreaching + # CIS 3.6 – Ensure a log metric filter and alarm exist for AWS Management Console authentication failures + - alarmName: CIS-3.6-ConsoleAuthenticationFailure + alarmDescription: Alarm exist for AWS Management Console authentication failures + snsAlertLevel: Low + metricName: ConsoleAuthenticationFailure + namespace: LogMetrics + comparisonOperator: GreaterThanOrEqualToThreshold + evaluationPeriods: 1 + period: 300 + statistic: Sum + threshold: 1 + treatMissingData: notBreaching + # CIS 3.7 – Ensure a log metric filter and alarm exist for disabling or scheduled deletion of customer created CMKs + - alarmName: CIS-3.7-DisableOrDeleteCMK + alarmDescription: Alarm for disabling or scheduled deletion of customer created CMKs + snsAlertLevel: Low + metricName: DisableOrDeleteCMK + namespace: LogMetrics + comparisonOperator: GreaterThanOrEqualToThreshold + evaluationPeriods: 1 + period: 300 + statistic: Sum + threshold: 1 + treatMissingData: notBreaching + # CIS 3.8 – Ensure a log metric filter and alarm exist for S3 bucket policy changes + - alarmName: CIS-3.8-S3BucketPolicyChanges. + alarmDescription: Alarm for S3 bucket policy changes + snsAlertLevel: Low + metricName: S3BucketPolicyChanges + namespace: LogMetrics + comparisonOperator: GreaterThanOrEqualToThreshold + evaluationPeriods: 1 + period: 300 + statistic: Average + threshold: 1 + treatMissingData: notBreaching + # CIS 3.9 – Ensure a log metric filter and alarm exist for AWS Config configuration changes + - alarmName: CIS-3.9-AWSConfigChanges + alarmDescription: Alarm for AWS Config configuration changes + snsAlertLevel: Low + metricName: AWSConfigChanges + namespace: LogMetrics + comparisonOperator: GreaterThanOrEqualToThreshold + evaluationPeriods: 1 + period: 300 + statistic: Sum + threshold: 1 + treatMissingData: notBreaching + # CIS 3.10 – Ensure a log metric filter and alarm exist for security group changes + - alarmName: CIS-3.10-SecurityGroupChanges + alarmDescription: Alarm for security group changes + snsAlertLevel: Low + metricName: SecurityGroupChanges + namespace: LogMetrics + comparisonOperator: GreaterThanOrEqualToThreshold + evaluationPeriods: 1 + period: 300 + statistic: Sum + threshold: 1 + treatMissingData: notBreaching + # CIS 3.11 – Ensure a log metric filter and alarm exist for changes to Network Access Control Lists (NACL) + - alarmName: CIS-3.11-NetworkACLChanges + alarmDescription: Alarm for changes to Network Access Control Lists (NACL) + snsAlertLevel: Low + metricName: NetworkACLChanges + namespace: LogMetrics + comparisonOperator: GreaterThanOrEqualToThreshold + evaluationPeriods: 1 + period: 300 + statistic: Sum + threshold: 1 + treatMissingData: notBreaching + # CIS 3.12 – Ensure a log metric filter and alarm exist for changes to network gateways + - alarmName: CIS-3.12-NetworkGatewayChanges + alarmDescription: Alarm for changes to network gateways + snsAlertLevel: Low + metricName: NetworkGatewayChanges + namespace: LogMetrics + comparisonOperator: GreaterThanOrEqualToThreshold + evaluationPeriods: 1 + period: 300 + statistic: Sum + threshold: 1 + treatMissingData: notBreaching + # CIS 3.13 – Ensure a log metric filter and alarm exist for route table changes + - alarmName: CIS-3.13-RouteTableChanges + alarmDescription: Alarm for route table changes + snsAlertLevel: Low + metricName: RouteTableChanges + namespace: LogMetrics + comparisonOperator: GreaterThanOrEqualToThreshold + evaluationPeriods: 1 + period: 300 + statistic: Average + threshold: 1 + treatMissingData: notBreaching + # CIS 3.14 – Ensure a log metric filter and alarm exist for VPC changes + - alarmName: CIS-3.14-VPCChanges + alarmDescription: Alarm for VPC changes + snsAlertLevel: Low + metricName: VPCChanges + namespace: LogMetrics + comparisonOperator: GreaterThanOrEqualToThreshold + evaluationPeriods: 1 + period: 300 + statistic: Sum + threshold: 1 + treatMissingData: notBreaching diff --git a/reference/sample-configurations/aws-best-practices/service-control-policies/deny-delete-vpc-flow-logs.json b/reference/sample-configurations/aws-best-practices/service-control-policies/deny-delete-vpc-flow-logs.json new file mode 100644 index 000000000..df4c5400d --- /dev/null +++ b/reference/sample-configurations/aws-best-practices/service-control-policies/deny-delete-vpc-flow-logs.json @@ -0,0 +1,15 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Deny", + "Action": ["ec2:DeleteFlowLogs", "logs:DeleteLogGroup", "logs:DeleteLogStream"], + "Resource": "*", + "Condition": { + "ArnNotLike": { + "aws:PrincipalArn": ["arn:aws:iam::*:role/cdk-accel-*"] + } + } + } + ] +} diff --git a/reference/sample-configurations/aws-best-practices/service-control-policies/guardrails-1.json b/reference/sample-configurations/aws-best-practices/service-control-policies/guardrails-1.json new file mode 100644 index 000000000..703e0077e --- /dev/null +++ b/reference/sample-configurations/aws-best-practices/service-control-policies/guardrails-1.json @@ -0,0 +1,91 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "ConfigRulesStatement", + "Effect": "Deny", + "Action": [ + "config:PutConfigRule", + "config:DeleteConfigRule", + "config:DeleteEvaluationResults", + "config:DeleteConfigurationAggregator", + "config:PutConfigurationAggregator" + ], + "Resource": "*", + "Condition": { + "ArnNotLike": { + "aws:PrincipalARN": [ + "arn:${PARTITION}:iam::*:role/${ACCELERATOR_PREFIX}-*", + "arn:${PARTITION}:iam::*:role/AWSControlTowerExecution", + "arn:${PARTITION}:iam::*:role/cdk-accel-*" + ] + } + } + }, + { + "Sid": "LambdaStatement", + "Effect": "Deny", + "Action": [ + "lambda:AddPermission", + "lambda:CreateEventSourceMapping", + "lambda:CreateFunction", + "lambda:DeleteEventSourceMapping", + "lambda:DeleteFunction", + "lambda:DeleteFunctionConcurrency", + "lambda:PutFunctionConcurrency", + "lambda:RemovePermission", + "lambda:UpdateEventSourceMapping", + "lambda:UpdateFunctionCode", + "lambda:UpdateFunctionConfiguration" + ], + "Resource": "arn:${PARTITION}:lambda:*:*:function:${ACCELERATOR_PREFIX}-*", + "Condition": { + "ArnNotLike": { + "aws:PrincipalARN": [ + "arn:${PARTITION}:iam::*:role/${ACCELERATOR_PREFIX}-*", + "arn:${PARTITION}:iam::*:role/AWSControlTowerExecution", + "arn:${PARTITION}:iam::*:role/cdk-accel-*" + ] + } + } + }, + { + "Sid": "SnsStatement", + "Effect": "Deny", + "Action": [ + "sns:AddPermission", + "sns:CreateTopic", + "sns:DeleteTopic", + "sns:RemovePermission", + "sns:SetTopicAttributes", + "sns:Subscribe", + "sns:Unsubscribe" + ], + "Resource": "arn:${PARTITION}:sns:*:*:aws-accelerator-*", + "Condition": { + "ArnNotLike": { + "aws:PrincipalARN": [ + "arn:${PARTITION}:iam::*:role/${ACCELERATOR_PREFIX}-*", + "arn:${PARTITION}:iam::*:role/AWSControlTowerExecution", + "arn:${PARTITION}:iam::*:role/cdk-accel-*" + ] + } + } + }, + { + "Sid": "EbsEncryptionStatement", + "Effect": "Deny", + "Action": ["ec2:DisableEbsEncryptionByDefault"], + "Resource": "*", + "Condition": { + "ArnNotLike": { + "aws:PrincipalARN": [ + "arn:${PARTITION}:iam::*:role/${ACCELERATOR_PREFIX}-*", + "arn:${PARTITION}:iam::*:role/AWSControlTowerExecution", + "arn:${PARTITION}:iam::*:role/cdk-accel-*" + ] + } + } + } + ] +} diff --git a/reference/sample-configurations/aws-best-practices/service-control-policies/guardrails-2.json b/reference/sample-configurations/aws-best-practices/service-control-policies/guardrails-2.json new file mode 100644 index 000000000..5d3a5a8ea --- /dev/null +++ b/reference/sample-configurations/aws-best-practices/service-control-policies/guardrails-2.json @@ -0,0 +1,144 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "IamSettingsStatement", + "Effect": "Deny", + "Action": [ + "iam:DeleteAccountPasswordPolicy", + "iam:UpdateAccountPasswordPolicy", + "iam:CreateAccountAlias", + "iam:DeleteAccountAlias" + ], + "Resource": "*", + "Condition": { + "ArnNotLike": { + "aws:PrincipalARN": [ + "arn:${PARTITION}:iam::*:role/${ACCELERATOR_PREFIX}-*", + "arn:${PARTITION}:iam::*:role/AWSControlTowerExecution", + "arn:${PARTITION}:iam::*:role/cdk-accel-*" + ] + } + } + }, + { + "Sid": "IamRolesStatement", + "Effect": "Deny", + "Action": [ + "iam:AttachRolePolicy", + "iam:CreateAccountAlias", + "iam:DeleteAccountAlias", + "iam:CreateUser", + "iam:DeleteUser", + "iam:CreateRole", + "iam:DeleteRole", + "iam:CreatePolicy", + "iam:DeletePolicy", + "iam:DeleteRolePermissionsBoundary", + "iam:DeleteRolePolicy", + "iam:DetachRolePolicy", + "iam:PutRolePermissionsBoundary", + "iam:PutRolePolicy", + "iam:UpdateAssumeRolePolicy", + "iam:UpdateRole", + "iam:UpdateRoleDescription" + ], + "Resource": [ + "arn:${PARTITION}:iam::*:role/${ACCELERATOR_PREFIX}-*" + ], + "Condition": { + "ArnNotLike": { + "aws:PrincipalARN": [ + "arn:${PARTITION}:iam::*:role/${ACCELERATOR_PREFIX}-*", + "arn:${PARTITION}:iam::*:role/AWSControlTowerExecution", + "arn:${PARTITION}:iam::*:role/cdk-accel-*" + ] + } + } + }, + { + "Sid": "GDSecHubServicesStatement", + "Effect": "Deny", + "Action": [ + "guardduty:DeleteDetector", + "guardduty:DeleteMembers", + "guardduty:UpdateDetector", + "guardduty:StopMonitoringMembers", + "guardduty:Disassociate*", + "securityhub:BatchDisableStandards", + "securityhub:DisableSecurityHub", + "securityhub:DeleteMembers", + "securityhub:Disassociate*" + ], + "Resource": "*", + "Condition": { + "ArnNotLike": { + "aws:PrincipalARN": [ + "arn:${PARTITION}:iam::*:role/${ACCELERATOR_PREFIX}-*", + "arn:${PARTITION}:iam::*:role/AWSControlTowerExecution", + "arn:${PARTITION}:iam::*:role/cdk-accel-*" + ] + } + } + }, + { + "Sid": "MacieServiceStatement", + "Effect": "Deny", + "Action": [ + "macie:AcceptInvitation", + "macie:CreateInvitations", + "macie:CreateMember", + "macie:DeclineInvitations", + "macie:DeleteInvitations", + "macie:DeleteMember", + "macie:DisableMacie", + "macie:DisableOrganizationAdminAccount", + "macie:Disassociate*", + "macie:Enable*", + "macie:UpdateMacieSession", + "macie:UpdateMemberSession", + "macie:UpdateOrganizationConfiguration" + ], + "Resource": "*", + "Condition": { + "ArnNotLike": { + "aws:PrincipalARN": [ + "arn:${PARTITION}:iam::*:role/${ACCELERATOR_PREFIX}-*", + "arn:${PARTITION}:iam::*:role/AWSControlTowerExecution", + "arn:${PARTITION}:iam::*:role/cdk-accel-*" + ] + } + } + }, + { + "Sid": "CloudFormationStatement", + "Effect": "Deny", + "Action": ["cloudformation:Delete*"], + "Resource": "arn:${PARTITION}:cloudformation:*:*:stack/${ACCELERATOR_PREFIX}-*", + "Condition": { + "ArnNotLike": { + "aws:PrincipalARN": [ + "arn:${PARTITION}:iam::*:role/${ACCELERATOR_PREFIX}-*", + "arn:${PARTITION}:iam::*:role/AWSControlTowerExecution", + "arn:${PARTITION}:iam::*:role/cdk-accel-*" + ] + } + } + }, + { + "Sid": "PreventSSMModification", + "Effect": "Deny", + "Action": ["ssm:DeleteParameters"], + "Resource": "*", + "Condition": { + "ArnNotLike": { + "aws:PrincipalARN": [ + "arn:${PARTITION}:iam::*:role/${ACCELERATOR_PREFIX}-*", + "arn:${PARTITION}:iam::*:role/AWSControlTowerExecution", + "arn:${PARTITION}:iam::*:role/cdk-accel-*" + ] + } + } + } + ] +} diff --git a/reference/sample-configurations/aws-best-practices/service-control-policies/quarantine.json b/reference/sample-configurations/aws-best-practices/service-control-policies/quarantine.json new file mode 100644 index 000000000..b88eace9f --- /dev/null +++ b/reference/sample-configurations/aws-best-practices/service-control-policies/quarantine.json @@ -0,0 +1,21 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "DenyAllAWSServicesExceptBreakglassRoles", + "Effect": "Deny", + "Action": "*", + "Resource": "*", + "Condition": { + "ArnNotLike": { + "aws:PrincipalArn": [ + "arn:${PARTITION}:iam::*:role/${MANAGEMENT_ACCOUNT_ACCESS_ROLE}", + "arn:${PARTITION}:iam::*:role/aws*", + "arn:${PARTITION}:iam::*:role/${ACCELERATOR_PREFIX}*", + "arn:${PARTITION}:iam::*:role/cdk-accel-*" + ] + } + } + } + ] +} diff --git a/reference/sample-configurations/aws-best-practices/ssm-documents/attach-iam-instance-profile.yaml b/reference/sample-configurations/aws-best-practices/ssm-documents/attach-iam-instance-profile.yaml new file mode 100644 index 000000000..543ba02cf --- /dev/null +++ b/reference/sample-configurations/aws-best-practices/ssm-documents/attach-iam-instance-profile.yaml @@ -0,0 +1,19 @@ +description: Associate AWS Iam Instance Profile to EC2 Instance +schemaVersion: '0.3' +assumeRole: '{{ AutomationAssumeRole }}' +parameters: + IamInstanceProfile: + type: String + InstanceId: + type: String + AutomationAssumeRole: + type: String +mainSteps: + - name: associateIamProfile + action: 'aws:executeAwsApi' + inputs: + Service: ec2 + Api: associate_iam_instance_profile + IamInstanceProfile: + Name: '{{ IamInstanceProfile }}' + InstanceId: '{{ InstanceId }}' \ No newline at end of file diff --git a/reference/sample-configurations/aws-best-practices/ssm-documents/attach-iam-role-policy.yaml b/reference/sample-configurations/aws-best-practices/ssm-documents/attach-iam-role-policy.yaml new file mode 100644 index 000000000..58cc55722 --- /dev/null +++ b/reference/sample-configurations/aws-best-practices/ssm-documents/attach-iam-role-policy.yaml @@ -0,0 +1,50 @@ +description: IAM Role Policy +schemaVersion: '0.3' +assumeRole: '{{ AutomationAssumeRole }}' +parameters: + ResourceId: + type: String + AWSManagedPolicies: + type: StringList + CustomerManagedPolicies: + type: StringList + minItems: 0 + default: [] + AutomationAssumeRole: + type: String +mainSteps: + - name: attachPolicy + action: 'aws:executeScript' + inputs: + Runtime: python3.7 + Handler: script_handler + Script: |- + import boto3 + partition = boto3.client("sts").get_caller_identity()['Arn'].split(":")[1] + iam = boto3.client("iam") + config = boto3.client("config") + def script_handler(events, context): + resource_id = events["ResourceId"] + response = config.batch_get_resource_config( + resourceKeys=[{ + 'resourceType': 'AWS::IAM::Role', + 'resourceId': resource_id + }] + ) + role_name = response["baseConfigurationItems"][0]['resourceName'] + aws_policy_names = events["AWSManagedPolicies"] + customer_policy_names = events["CustomerManagedPolicies"] + for policy in aws_policy_names: + iam.attach_role_policy( + PolicyArn="arn:%s:iam::aws:policy/%s" %(partition, policy), + RoleName=role_name + ) + for policy in customer_policy_names: + iam.attach_role_policy( + PolicyArn=policy, + RoleName=role_name + ) + InputPayload: + ResourceId: '{{ ResourceId }}' + AWSManagedPolicies: '{{ AWSManagedPolicies }}' + CustomerManagedPolicies: '{{ CustomerManagedPolicies }}' \ No newline at end of file diff --git a/reference/sample-configurations/aws-best-practices/ssm-documents/s3-encryption.yaml b/reference/sample-configurations/aws-best-practices/ssm-documents/s3-encryption.yaml new file mode 100644 index 000000000..79bc510bb --- /dev/null +++ b/reference/sample-configurations/aws-best-practices/ssm-documents/s3-encryption.yaml @@ -0,0 +1,28 @@ +description: Enables Encryption on S3 Bucket +schemaVersion: "0.3" +assumeRole: "{{ AutomationAssumeRole }}" +parameters: + BucketName: + type: String + description: (Required) The name of the S3 Bucket whose content will be encrypted. + KMSMasterKey: + type: String + description: (Optional) AWS Key Management Service (KMS) customer master key ARN to use for the default encryption. + AutomationAssumeRole: + type: String + description: (Optional) The ARN of the role that allows Automation to perform the actions on your behalf. + default: "" +mainSteps: +- name: PutBucketEncryption + action: aws:executeAwsApi + inputs: + Service: s3 + Api: PutBucketEncryption + Bucket: "{{BucketName}}" + ServerSideEncryptionConfiguration: + Rules: + - + ApplyServerSideEncryptionByDefault: + SSEAlgorithm: "aws:kms" + KMSMasterKeyID: "{{KMSMasterKey}}" + isEnd: true \ No newline at end of file diff --git a/reference/sample-configurations/aws-best-practices/ssm-documents/ssm-elb-enable-logging.yaml b/reference/sample-configurations/aws-best-practices/ssm-documents/ssm-elb-enable-logging.yaml new file mode 100644 index 000000000..25a0fab05 --- /dev/null +++ b/reference/sample-configurations/aws-best-practices/ssm-documents/ssm-elb-enable-logging.yaml @@ -0,0 +1,44 @@ +description: Enable logging on Elastic Load-Balancer +schemaVersion: '0.3' +assumeRole: '{{ AutomationAssumeRole }}' +parameters: + LogDestination: + type: String + LoadBalancerArn: + type: String + AutomationAssumeRole: + type: String +mainSteps: + - name: getAccount + action: 'aws:executeAwsApi' + inputs: + Service: sts + Api: get_caller_identity + outputs: + - Name: Id + Selector: $.Account + Type: String + - name: getLoadBalancer + action: 'aws:executeAwsApi' + inputs: + Service: elbv2 + Api: describe_load_balancers + LoadBalancerArns: + - '{{ LoadBalancerArn }}' + outputs: + - Name: Name + Selector: $.LoadBalancers[0].LoadBalancerName + Type: String + - name: enableLogging + action: 'aws:executeAwsApi' + inputs: + Service: elbv2 + Api: modify_load_balancer_attributes + LoadBalancerArn: '{{ LoadBalancerArn }}' + Attributes: + - Key: access_logs.s3.enabled + Value: 'true' + - Key: access_logs.s3.bucket + Value: '{{ LogDestination }}' + - Key: access_logs.s3.prefix + Value: '{{ getAccount.Id }}/elb-{{ getLoadBalancer.Name }}' diff --git a/reference/sample-configurations/aws-best-practices/tagging-policies/org-tag-policy.json b/reference/sample-configurations/aws-best-practices/tagging-policies/org-tag-policy.json new file mode 100644 index 000000000..e49fbb0f4 --- /dev/null +++ b/reference/sample-configurations/aws-best-practices/tagging-policies/org-tag-policy.json @@ -0,0 +1,15 @@ +{ + "tags": { + "costcenter": { + "tag_key": { + "@@assign": "CostCenter" + }, + "tag_value": { + "@@assign": [ + "100", + "200" + ] + } + } + } +} diff --git a/reference/sample-configurations/aws-best-practices/test-configuration/config.yaml b/reference/sample-configurations/aws-best-practices/test-configuration/config.yaml new file mode 100644 index 000000000..482e91447 --- /dev/null +++ b/reference/sample-configurations/aws-best-practices/test-configuration/config.yaml @@ -0,0 +1,22 @@ +tests: + - name: validate main transit gateway + description: Validate Main Transit Gateway + suite: network + testTarget: validateTransitGateway + expect: PASS + parameters: + name: Main + accountId: '' + region: us-east-1 + amazonSideAsn: '65521' + dnsSupport: enable + vpnEcmpSupport: enable + defaultRouteTableAssociation: disable + defaultRouteTablePropagation: disable + autoAcceptSharingAttachments: enable + routeTableNames: + - core + - segregated + - shared + - standalone + shareTargetAccountIds: [] \ No newline at end of file diff --git a/reference/sample-configurations/aws-best-practices/vpc-endpoint-policies/default.json b/reference/sample-configurations/aws-best-practices/vpc-endpoint-policies/default.json new file mode 100644 index 000000000..a812fa1f3 --- /dev/null +++ b/reference/sample-configurations/aws-best-practices/vpc-endpoint-policies/default.json @@ -0,0 +1,10 @@ +{ + "Statement": [ + { + "Effect": "Allow", + "Principal": "*", + "Action": "*", + "Resource": "*" + } + ] +} \ No newline at end of file diff --git a/reference/sample-configurations/aws-best-practices/vpc-endpoint-policies/ec2.json b/reference/sample-configurations/aws-best-practices/vpc-endpoint-policies/ec2.json new file mode 100644 index 000000000..4c707d8de --- /dev/null +++ b/reference/sample-configurations/aws-best-practices/vpc-endpoint-policies/ec2.json @@ -0,0 +1,10 @@ +{ + "Statement": [ + { + "Effect": "Allow", + "Principal": "*", + "Action": "ec2:*", + "Resource": "*" + } + ] +} \ No newline at end of file diff --git a/source/.eslintrc.json b/source/.eslintrc.json new file mode 100644 index 000000000..bd7f37c32 --- /dev/null +++ b/source/.eslintrc.json @@ -0,0 +1,25 @@ +{ + "env": { + "es2021": true, + "node": true + }, + "root": true, + "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:prettier/recommended"], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 12, + "sourceType": "module" + }, + "plugins": ["@typescript-eslint"], + "rules": { + "dot-notation": "off", + "no-case-declarations": "off", + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/camelcase": "off", + "@typescript-eslint/no-var-requires": 0, + "@typescript-eslint/ban-ts-comment": "off" + }, + "globals": { + "require": true + } +} diff --git a/source/.husky/pre-commit b/source/.husky/pre-commit new file mode 100755 index 000000000..4994aa1d9 --- /dev/null +++ b/source/.husky/pre-commit @@ -0,0 +1,6 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +wget -v "https://s3.amazonaws.com/viperlight-scanner/latest/viperlight.zip" && unzip -qo viperlight.zip -d ../viperlight && rm -r ./viperlight.zip && ../viperlight/bin/viperlight scan + +cd source && yarn run test && yarn lerna run precommit --stream \ No newline at end of file diff --git a/source/.prettierrc.json b/source/.prettierrc.json new file mode 100644 index 000000000..c2f87d525 --- /dev/null +++ b/source/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "tabWidth": 2, + "printWidth": 120, + "singleQuote": true, + "trailingComma": "all", + "arrowParens": "avoid" +} diff --git a/source/lerna.json b/source/lerna.json new file mode 100644 index 000000000..b182c6695 --- /dev/null +++ b/source/lerna.json @@ -0,0 +1,6 @@ +{ + "npmClient": "yarn", + "useWorkspaces": true, + "rejectCycles": "true", + "version": "2.0.0" +} diff --git a/source/package.json b/source/package.json new file mode 100644 index 000000000..1b004993f --- /dev/null +++ b/source/package.json @@ -0,0 +1,81 @@ +{ + "name": "landing-zone-accelerator-on-aws", + "version": "1.0.0", + "description": "Landing Zone Accelerator on AWS", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "dependencies": { + "fs-extra": "10.0.0", + "jest": "27.4.3", + "jsii": "1.47.0", + "jsii-pacmak": "1.47.0" + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "@typescript-eslint/eslint-plugin": "5.6.0", + "@typescript-eslint/parser": "5.6.0", + "aws-cdk-lib": "2.16.0", + "constructs": "10.0.12", + "esbuild": "0.14.2", + "eslint-config-prettier": "8.3.0", + "eslint-plugin-jest": "25.3.0", + "eslint-plugin-prettier": "4.0.0", + "husky-init": "7.0.0", + "jest": "27.4.3", + "jest-junit": "13.0.0", + "lerna": "^4.0.0", + "ts-jest": "27.1.1", + "ts-node": "10.4.0", + "typedoc": "0.22.11", + "typescript": "4.5.2" + }, + "peerDependencies": { + "eslint": "8.4.1" + }, + "scripts": { + "cleanup": "lerna run cleanup --stream && rm -rf node_modules", + "cleanup:tsc": "lerna run cleanup:tsc --stream", + "build": "lerna run build --stream", + "test": "lerna run test --stream", + "test:clean": "rm -f ./test-reports/*.xml", + "docs": "yarn run typedoc --entryPointStrategy packages './packages/@aws-accelerator/*' './packages/@aws-cdk-extensions/*' --name 'Landing Zone Accelerator on AWS' --includeVersion --disableSources --logLevel Verbose", + "viper-scan": "cd .. && wget -v \"https://s3.amazonaws.com/viperlight-scanner/latest/viperlight.zip\" && unzip -qo viperlight.zip -d ../viperlight && rm -r ./viperlight.zip && ../viperlight/bin/viperlight scan", + "prepare": "cd ../ && husky install source/.husky", + "postinstall": "cd ../ && husky install source/.husky && cd source", + "lint": "lerna run lint --stream", + "prettier": "lerna run prettier --stream" + }, + "workspaces": { + "packages": [ + "./packages/@aws-accelerator/accelerator", + "./packages/@aws-accelerator/accelerator/lib/lambdas/*", + "./packages/@aws-accelerator/config", + "./packages/@aws-accelerator/constructs", + "./packages/@aws-accelerator/constructs/lib/aws-*/*", + "./packages/@aws-accelerator/installer", + "./packages/@aws-accelerator/tester", + "./packages/@aws-accelerator/tester/lambdas", + "./packages/@aws-accelerator/tools", + "./packages/@aws-accelerator/utils", + "./packages/@aws-cdk-extensions/cdk-extensions", + "./packages/@aws-cdk-extensions/cdk-plugin-assume-role" + ], + "nohoist": [ + "**/deepmerge", + "**/deepmerge/**", + "**/npmlog", + "**/npmlog/**", + "**/@types/npmlog", + "**/@types/npmlog/**", + "**/deep-diff", + "**/deep-diff/**", + "**/@types/deep-diff", + "**/@types/deep-diff/**" + ] + } +} diff --git a/source/packages/@aws-accelerator/accelerator/.npmignore b/source/packages/@aws-accelerator/accelerator/.npmignore new file mode 100644 index 000000000..bd6c7fe71 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/.npmignore @@ -0,0 +1,2 @@ +*.ts +!*.d.ts \ No newline at end of file diff --git a/source/packages/@aws-accelerator/accelerator/README.md b/source/packages/@aws-accelerator/accelerator/README.md new file mode 100644 index 000000000..d537ffca8 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/README.md @@ -0,0 +1 @@ +# @aws-accelerator/accelerator diff --git a/source/packages/@aws-accelerator/accelerator/bin/app.ts b/source/packages/@aws-accelerator/accelerator/bin/app.ts new file mode 100644 index 000000000..3a67f6762 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/bin/app.ts @@ -0,0 +1,510 @@ +#!/usr/bin/env node + +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import 'source-map-support/register'; + +import * as cdk from 'aws-cdk-lib'; +import { AwsSolutionsChecks } from 'cdk-nag'; +import { IConstruct } from 'constructs'; +import { version } from '../../../../package.json'; + +import { + AccountsConfig, + GlobalConfig, + IamConfig, + NetworkConfig, + OrganizationConfig, + SecurityConfig, +} from '@aws-accelerator/config'; + +import { AcceleratorStackNames } from '../lib/accelerator'; +import { AcceleratorStage } from '../lib/accelerator-stage'; +import { Logger } from '../lib/logger'; +import { AccountsStack } from '../lib/stacks/accounts-stack'; +import { FinalizeStack } from '../lib/stacks/finalize-stack'; +import { KeyStack } from '../lib/stacks/key-stack'; +import { LoggingStack } from '../lib/stacks/logging-stack'; +import { NetworkAssociationsStack } from '../lib/stacks/network-associations-stack'; +import { NetworkPrepStack } from '../lib/stacks/network-prep-stack'; +import { NetworkVpcDnsStack } from '../lib/stacks/network-vpc-dns-stack'; +import { NetworkVpcEndpointsStack } from '../lib/stacks/network-vpc-endpoints-stack'; +import { NetworkVpcStack } from '../lib/stacks/network-vpc-stack'; +import { OperationsStack } from '../lib/stacks/operations-stack'; +import { OrganizationsStack } from '../lib/stacks/organizations-stack'; +import { PipelineStack } from '../lib/stacks/pipeline-stack'; +import { PrepareStack } from '../lib/stacks/prepare-stack'; +import { SecurityAuditStack } from '../lib/stacks/security-audit-stack'; +import { SecurityResourcesStack } from '../lib/stacks/security-resources-stack'; +import { SecurityStack } from '../lib/stacks/security-stack'; +import { TesterPipelineStack } from '../lib/stacks/tester-pipeline-stack'; + +process.on( + 'unhandledRejection', + ( + reason, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _, + ) => { + console.error(reason); + // eslint-disable-next-line no-process-exit + process.exit(1); + }, +); + +export class GovCloudOverrides implements cdk.IAspect { + public visit(node: IConstruct): void { + if (node instanceof cdk.aws_logs.CfnLogGroup) { + node.addPropertyDeletionOverride('KmsKeyId'), node.addPropertyDeletionOverride('Tags'); + } + } +} + +export class IsobOverrides implements cdk.IAspect { + public visit(node: IConstruct): void { + if (node instanceof cdk.aws_ec2.CfnFlowLog) { + node.addPropertyDeletionOverride('LogFormat'), + node.addPropertyDeletionOverride('Tags'), + node.addPropertyDeletionOverride('MaxAggregationInterval'); + } + if (node instanceof cdk.aws_logs.CfnLogGroup) { + node.addPropertyDeletionOverride('KmsKeyId'), node.addPropertyDeletionOverride('Tags'); + } + if (node instanceof cdk.aws_s3.CfnBucket) { + node.addPropertyDeletionOverride('PublicAccessBlockConfiguration'), + node.addPropertyDeletionOverride('OwnershipControls'); + } + } +} + +async function main() { + Logger.info('[app] Begin Accelerator CDK App'); + const app = new cdk.App(); + cdk.Aspects.of(app).add(new AwsSolutionsChecks()); + + // + // Read in context inputs + // + const configDirPath = app.node.tryGetContext('config-dir'); + const stage = app.node.tryGetContext('stage'); + const account = app.node.tryGetContext('account'); + const region = app.node.tryGetContext('region'); + const partition = app.node.tryGetContext('partition'); + + let globalRegion = 'us-east-1'; + + if (partition === 'aws-us-gov') { + cdk.Aspects.of(app).add(new GovCloudOverrides()); + globalRegion = 'us-gov-west-1'; + } + + if (partition === 'aws-iso-b') { + cdk.Aspects.of(app).add(new IsobOverrides()); + } + + const includeStage = (props: { stage: string; account: string; region: string }): boolean => { + if (stage === undefined) { + // Do not include PIPELINE or TESTER_PIPELINE in full synth/diff + if (props.stage === AcceleratorStage.PIPELINE || props.stage === AcceleratorStage.TESTER_PIPELINE) { + return false; + } + return true; // No stage, return all other stacks + } + if (stage === props.stage) { + if (account === undefined && region === undefined) { + return true; // No account or region, return all stacks for synth/diff + } + if (props.account === account && props.region === region) { + return true; + } + } + return false; + }; + + // + // PIPELINE Stack + // + if (includeStage({ stage: AcceleratorStage.PIPELINE, account, region })) { + const sourceRepository = process.env['ACCELERATOR_REPOSITORY_SOURCE'] ?? 'github'; + const sourceRepositoryOwner = process.env['ACCELERATOR_REPOSITORY_OWNER'] ?? 'awslabs'; + const sourceRepositoryName = process.env['ACCELERATOR_REPOSITORY_NAME'] ?? 'landing-zone-accelerator-on-aws'; + const sourceBranchName = process.env['ACCELERATOR_REPOSITORY_BRANCH_NAME']; + const enableApprovalStage = process.env['ACCELERATOR_ENABLE_APPROVAL_STAGE'] + ? process.env['ACCELERATOR_ENABLE_APPROVAL_STAGE'] === 'Yes' + : true; + + // Verify ENV vars are set + if (!sourceRepositoryName || !sourceBranchName) { + throw new Error( + 'Attempting to deploy pipeline stage and environment variables are not set [ACCELERATOR_REPOSITORY_NAME, ACCELERATOR_REPOSITORY_BRANCH_NAME]', + ); + } + + new PipelineStack( + app, + process.env['ACCELERATOR_QUALIFIER'] + ? `${process.env['ACCELERATOR_QUALIFIER']}-${AcceleratorStage.PIPELINE}-stack-${account}-${region}` + : `${AcceleratorStackNames[stage]}-${account}-${region}`, + { + env: { account, region }, + description: `(SO0199-pipeline) Landing Zone Accelerator on AWS. Version ${version}.`, + sourceRepository, + sourceRepositoryOwner, + sourceRepositoryName, + sourceBranchName, + enableApprovalStage, + qualifier: process.env['ACCELERATOR_QUALIFIER'], + managementAccountId: process.env['MANAGEMENT_ACCOUNT_ID']!, + managementAccountRoleName: process.env['MANAGEMENT_ACCOUNT_ROLE_NAME']!, + managementAccountEmail: process.env['MANAGEMENT_ACCOUNT_EMAIL']!, + logArchiveAccountEmail: process.env['LOG_ARCHIVE_ACCOUNT_EMAIL']!, + auditAccountEmail: process.env['AUDIT_ACCOUNT_EMAIL']!, + approvalStageNotifyEmailList: process.env['APPROVAL_STAGE_NOTIFY_EMAIL_LIST'], + partition, + }, + ); + } + + // + // TESTER Stack + // + if (includeStage({ stage: AcceleratorStage.TESTER_PIPELINE, account, region })) { + if (process.env['ACCELERATOR_REPOSITORY_NAME'] && process.env['ACCELERATOR_REPOSITORY_BRANCH_NAME']) { + new TesterPipelineStack( + app, + process.env['ACCELERATOR_QUALIFIER'] + ? `${process.env['ACCELERATOR_QUALIFIER']}-${AcceleratorStage.TESTER_PIPELINE}-stack-${account}-${region}` + : `${AcceleratorStackNames[stage]}-${account}-${region}`, + { + env: { account, region }, + description: `(SO0199-tester) Landing Zone Accelerator on AWS. Version ${version}.`, + sourceRepositoryName: process.env['ACCELERATOR_REPOSITORY_NAME']!, + sourceBranchName: process.env['ACCELERATOR_REPOSITORY_BRANCH_NAME']!, + managementCrossAccountRoleName: process.env['MANAGEMENT_CROSS_ACCOUNT_ROLE_NAME']!, + qualifier: process.env['ACCELERATOR_QUALIFIER'], + managementAccountId: process.env['MANAGEMENT_ACCOUNT_ID'], + managementAccountRoleName: process.env['MANAGEMENT_ACCOUNT_ROLE_NAME'], + }, + ); + } + } + + if (configDirPath) { + // + // Create properties to be used by AcceleratorStack types + // + const props = { + configDirPath, + accountsConfig: AccountsConfig.load(configDirPath), + globalConfig: GlobalConfig.load(configDirPath), + iamConfig: IamConfig.load(configDirPath), + networkConfig: NetworkConfig.load(configDirPath), + organizationConfig: OrganizationConfig.load(configDirPath), + securityConfig: SecurityConfig.load(configDirPath), + partition: partition, + qualifier: process.env['ACCELERATOR_QUALIFIER'], + configCommitId: process.env['CONFIG_COMMIT_ID'], + }; + + // + // Load in account IDs using the Organizations client if not provided as + // inputs in accountsConfig + // + await props.accountsConfig.loadAccountIds(partition); + + // + // Load in organizational unit IDs using the Organizations client if not + // provided as inputs in accountsConfig + // + await props.organizationConfig.loadOrganizationalUnitIds(partition); + + const homeRegion = props.globalConfig.homeRegion; + const managementAccountId = props.accountsConfig.getManagementAccountId(); + const auditAccountId = props.accountsConfig.getAuditAccountId(); + + // + // PREPARE Stack + // + if (includeStage({ stage: AcceleratorStage.PREPARE, account: managementAccountId, region: homeRegion })) { + new PrepareStack(app, `${AcceleratorStackNames[AcceleratorStage.PREPARE]}-${managementAccountId}-${homeRegion}`, { + env: { + account: managementAccountId, + region: homeRegion, + }, + description: `(SO0199-prepare) Landing Zone Accelerator on AWS. Version ${version}.`, + synthesizer: new cdk.DefaultStackSynthesizer({ + generateBootstrapVersionRule: false, + }), + ...props, + }); + } + + // + // FINALIZE Stack + // + if (includeStage({ stage: AcceleratorStage.FINALIZE, account: managementAccountId, region: globalRegion })) { + new FinalizeStack( + app, + `${AcceleratorStackNames[AcceleratorStage.FINALIZE]}-${managementAccountId}-${globalRegion}`, + { + env: { + account: managementAccountId, + region: globalRegion, + }, + description: `(SO0199-finalize) Landing Zone Accelerator on AWS. Version ${version}.`, + synthesizer: new cdk.DefaultStackSynthesizer({ + generateBootstrapVersionRule: false, + }), + ...props, + }, + ); + } + + // + // ACCOUNTS Stack + // + if (includeStage({ stage: AcceleratorStage.ACCOUNTS, account: managementAccountId, region: globalRegion })) { + new AccountsStack( + app, + `${AcceleratorStackNames[AcceleratorStage.ACCOUNTS]}-${managementAccountId}-${globalRegion}`, + { + env: { + account: managementAccountId, + region: globalRegion, + }, + description: `(SO0199-accounts) Landing Zone Accelerator on AWS. Version ${version}.`, + ...props, + }, + ); + } + + // + // ORGANIZATIONS Stack + // + for (const enabledRegion of props.globalConfig.enabledRegions) { + if ( + includeStage({ stage: AcceleratorStage.ORGANIZATIONS, account: managementAccountId, region: enabledRegion }) + ) { + new OrganizationsStack( + app, + `${AcceleratorStackNames[AcceleratorStage.ORGANIZATIONS]}-${managementAccountId}-${enabledRegion}`, + { + env: { + account: managementAccountId, + region: enabledRegion, + }, + description: `(SO0199-organizations) Landing Zone Accelerator on AWS. Version ${version}.`, + ...props, + }, + ); + } + } + + // + // KEY and SECURITY AUDIT Stack + // + for (const enabledRegion of props.globalConfig.enabledRegions) { + if (includeStage({ stage: AcceleratorStage.KEY, account: auditAccountId, region: enabledRegion })) { + new KeyStack(app, `${AcceleratorStackNames[AcceleratorStage.KEY]}-${auditAccountId}-${enabledRegion}`, { + env: { + account: auditAccountId, + region: enabledRegion, + }, + description: `(SO0199-key) Landing Zone Accelerator on AWS. Version ${version}.`, + ...props, + }); + } + + if (includeStage({ stage: AcceleratorStage.SECURITY_AUDIT, account: auditAccountId, region: enabledRegion })) { + new SecurityAuditStack( + app, + `${AcceleratorStackNames[AcceleratorStage.SECURITY_AUDIT]}-${auditAccountId}-${enabledRegion}`, + { + env: { + account: auditAccountId, + region: enabledRegion, + }, + description: `(SO0199-securityaudit) Landing Zone Accelerator on AWS. Version ${version}.`, + ...props, + }, + ); + } + } + + for (const enabledRegion of props.globalConfig.enabledRegions) { + let accountId = ''; + for (const accountItem of [...props.accountsConfig.mandatoryAccounts, ...props.accountsConfig.workloadAccounts]) { + try { + accountId = props.accountsConfig.getAccountId(accountItem.name); + } catch (error) { + continue; + } + const env = { + account: accountId, + region: enabledRegion, + }; + + // + // LOGGING Stack + // + if (includeStage({ stage: AcceleratorStage.LOGGING, account: accountId, region: enabledRegion })) { + new LoggingStack(app, `${AcceleratorStackNames[AcceleratorStage.LOGGING]}-${accountId}-${enabledRegion}`, { + env, + description: `(SO0199-logging) Landing Zone Accelerator on AWS. Version ${version}.`, + synthesizer: new cdk.DefaultStackSynthesizer({ + generateBootstrapVersionRule: false, + }), + ...props, + }); + } + + // + // SECURITY Stack + // + if (includeStage({ stage: AcceleratorStage.SECURITY, account: accountId, region: enabledRegion })) { + new SecurityStack(app, `${AcceleratorStackNames[AcceleratorStage.SECURITY]}-${accountId}-${enabledRegion}`, { + env, + description: `(SO0199-security) Landing Zone Accelerator on AWS. Version ${version}.`, + synthesizer: new cdk.DefaultStackSynthesizer({ + generateBootstrapVersionRule: false, + }), + ...props, + }); + } + + // + // OPERATIONS Stack + // + if (includeStage({ stage: AcceleratorStage.OPERATIONS, account: accountId, region: enabledRegion })) { + new OperationsStack( + app, + `${AcceleratorStackNames[AcceleratorStage.OPERATIONS]}-${accountId}-${enabledRegion}`, + { + env, + description: `(SO0199-operations) Landing Zone Accelerator on AWS. Version ${version}.`, + synthesizer: new cdk.DefaultStackSynthesizer({ + generateBootstrapVersionRule: false, + }), + ...props, + }, + ); + } + + // + // NETWORK PREP Stack + // + if (includeStage({ stage: AcceleratorStage.NETWORK_PREP, account: accountId, region: enabledRegion })) { + new NetworkPrepStack( + app, + `${AcceleratorStackNames[AcceleratorStage.NETWORK_PREP]}-${accountId}-${enabledRegion}`, + { + env, + description: `(SO0199-networkprep) Landing Zone Accelerator on AWS. Version ${version}.`, + synthesizer: new cdk.DefaultStackSynthesizer({ + generateBootstrapVersionRule: false, + }), + ...props, + }, + ); + } + + // + // SECURITY_RESOURCES Stack + // + if (includeStage({ stage: AcceleratorStage.SECURITY_RESOURCES, account: accountId, region: enabledRegion })) { + new SecurityResourcesStack( + app, + `${AcceleratorStackNames[AcceleratorStage.SECURITY_RESOURCES]}-${accountId}-${enabledRegion}`, + { + env, + description: `(SO0199-securityresources) Landing Zone Accelerator on AWS. Version ${version}.`, + synthesizer: new cdk.DefaultStackSynthesizer({ + generateBootstrapVersionRule: false, + }), + ...props, + }, + ); + } + + // + // NETWORK VPC Stack + // + if (includeStage({ stage: AcceleratorStage.NETWORK_VPC, account: accountId, region: enabledRegion })) { + const vpcStack = new NetworkVpcStack( + app, + `${AcceleratorStackNames[AcceleratorStage.NETWORK_VPC]}-${accountId}-${enabledRegion}`, + { + env, + description: `(SO0199-networkvpc) Landing Zone Accelerator on AWS. Version ${version}.`, + synthesizer: new cdk.DefaultStackSynthesizer({ + generateBootstrapVersionRule: false, + }), + ...props, + }, + ); + + const endpointsStack = new NetworkVpcEndpointsStack( + app, + `${AcceleratorStackNames[AcceleratorStage.NETWORK_VPC_ENDPOINTS]}-${accountId}-${enabledRegion}`, + { + env, + description: `(SO0199-networkendpoints) Landing Zone Accelerator on AWS. Version ${version}.`, + synthesizer: new cdk.DefaultStackSynthesizer({ + generateBootstrapVersionRule: false, + }), + ...props, + }, + ); + endpointsStack.addDependency(vpcStack); + + const dnsStack = new NetworkVpcDnsStack( + app, + `${AcceleratorStackNames[AcceleratorStage.NETWORK_VPC_DNS]}-${accountId}-${enabledRegion}`, + { + env, + description: `(SO0199-networkdns) Landing Zone Accelerator on AWS. Version ${version}.`, + synthesizer: new cdk.DefaultStackSynthesizer({ + generateBootstrapVersionRule: false, + }), + ...props, + }, + ); + dnsStack.addDependency(endpointsStack); + } + + // + // NETWORK ASSOCIATIONS Stack + // + if (includeStage({ stage: AcceleratorStage.NETWORK_ASSOCIATIONS, account: accountId, region: enabledRegion })) { + new NetworkAssociationsStack( + app, + `${AcceleratorStackNames[AcceleratorStage.NETWORK_ASSOCIATIONS]}-${accountId}-${enabledRegion}`, + { + env, + description: `(SO0199-networkassociations) Landing Zone Accelerator on AWS. Version ${version}.`, + synthesizer: new cdk.DefaultStackSynthesizer({ + generateBootstrapVersionRule: false, + }), + ...props, + }, + ); + } + } + } + } + + Logger.info('[app] End Accelerator CDK App'); +} + +main(); diff --git a/source/packages/@aws-accelerator/accelerator/cdk.json b/source/packages/@aws-accelerator/accelerator/cdk.json new file mode 100644 index 000000000..fe5d151e0 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/cdk.json @@ -0,0 +1,6 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/app.ts", + "context": { + "@aws-cdk/core:bootstrapQualifier": "accel" + } +} diff --git a/source/packages/@aws-accelerator/accelerator/cdk.ts b/source/packages/@aws-accelerator/accelerator/cdk.ts new file mode 100644 index 000000000..1fc2b4220 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/cdk.ts @@ -0,0 +1,92 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as config from '@aws-accelerator/config'; +import * as fs from 'fs'; +import mri from 'mri'; +import process from 'process'; +import { Accelerator } from './lib/accelerator'; +import { AcceleratorStage } from './lib/accelerator-stage'; +import { AcceleratorToolkit } from './lib/toolkit'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +process.on('unhandledRejection', (reason, _) => { + console.error(reason); + // eslint-disable-next-line no-process-exit + process.exit(1); +}); + +const usage = `Usage: cdk.ts --stage STAGE --config-dir CONFIG_DIRECTORY [--account ACCOUNT] [--region REGION]`; + +const args = mri(process.argv.slice(2), { + boolean: [], + string: ['require-approval', 'config-dir', 'partition', 'stage', 'account', 'region', 'app'], + alias: { + c: 'config-dir', + s: 'stage', + a: 'account', + r: 'region', + p: 'app', + }, +}); + +const commands = args['_']; +const requireApproval = args['require-approval']; +const configDirPath = args['config-dir']; +const partition = args['partition']; +const stage = args['stage']; +const account = args['account']; +const region = args['region']; +const app = args['app']; + +// +// Validate args: must specify a command +// +if (commands.length === 0) { + console.log(' not set'); + throw new Error(usage); +} + +// +// Validate args: verify command against our sub-list +// +if (!AcceleratorToolkit.isSupportedCommand(commands[0])) { + throw new Error(`Invalid command: ${commands[0]}`); +} + +// +// Validate args: verify config directory +// +if (stage !== AcceleratorStage.PIPELINE && stage !== AcceleratorStage.TESTER_PIPELINE) { + if (config === undefined || !fs.existsSync(configDirPath)) { + console.log(`Invalid --config-dir ${configDirPath}`); + throw new Error(usage); + } +} + +// +// Execute the Accelerator engine +// +Accelerator.run({ + command: commands[0], + configDirPath, + stage, + account, + region, + partition, + requireApproval, + app, +}).catch(function (err) { + console.log(err.message); + process.exit(1); +}); diff --git a/source/packages/@aws-accelerator/accelerator/index.ts b/source/packages/@aws-accelerator/accelerator/index.ts new file mode 100644 index 000000000..0dbd1c0e2 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/index.ts @@ -0,0 +1,32 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +export * from './lib/accelerator'; +export * from './lib/accelerator-stage'; +export * from './lib/config-repository'; +export * from './lib/logger'; +export * from './lib/pipeline'; +export * from './lib/stacks/accelerator-stack'; +export * from './lib/stacks/accounts-stack'; +export * from './lib/stacks/network-associations-stack'; +export * from './lib/stacks/network-prep-stack'; +export * from './lib/stacks/network-vpc-stack'; +export * from './lib/stacks/operations-stack'; +export * from './lib/stacks/organizations-stack'; +export * from './lib/stacks/pipeline-stack'; +export * from './lib/stacks/prepare-stack'; +export * from './lib/stacks/finalize-stack'; +export * from './lib/stacks/security-audit-stack'; +export * from './lib/stacks/security-stack'; +export * from './lib/toolkit'; +export * from './lib/validate-environment-config'; diff --git a/source/packages/@aws-accelerator/accelerator/jest.config.js b/source/packages/@aws-accelerator/accelerator/jest.config.js new file mode 100644 index 000000000..0e8a6842d --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/jest.config.js @@ -0,0 +1,6 @@ +const base = require('../../../jest.config.base'); +const packageJson = require('./package.json'); + +module.exports = { + ...base.getJestJunitConfig(packageJson.name), +}; diff --git a/source/packages/@aws-accelerator/accelerator/lib/accelerator-stage.ts b/source/packages/@aws-accelerator/accelerator/lib/accelerator-stage.ts new file mode 100644 index 000000000..8b4ed05e5 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/lib/accelerator-stage.ts @@ -0,0 +1,41 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +export enum AcceleratorStage { + PIPELINE = 'pipeline', + /** + * Accelerator Tester Pipeline + */ + TESTER_PIPELINE = 'tester-pipeline', + /** + * Prepare Stage - Verify the configuration files, environment and create accounts + */ + PREPARE = 'prepare', + ORGANIZATIONS = 'organizations', + KEY = 'key', + LOGGING = 'logging', + /** + * Accounts Stage - Handle all Organization and Accounts actions + */ + ACCOUNTS = 'accounts', + DEPENDENCIES = 'dependencies', + SECURITY = 'security', + SECURITY_RESOURCES = 'security-resources', + OPERATIONS = 'operations', + NETWORK_PREP = 'network-prep', + NETWORK_VPC = 'network-vpc', + NETWORK_VPC_ENDPOINTS = 'network-vpc-endpoints', + NETWORK_VPC_DNS = 'network-vpc-dns', + NETWORK_ASSOCIATIONS = 'network-associations', + SECURITY_AUDIT = 'security-audit', + FINALIZE = 'finalize', +} diff --git a/source/packages/@aws-accelerator/accelerator/lib/accelerator.ts b/source/packages/@aws-accelerator/accelerator/lib/accelerator.ts new file mode 100644 index 000000000..f14890bcd --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/lib/accelerator.ts @@ -0,0 +1,419 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { RequireApproval } from 'aws-cdk/lib/diff'; +import { PluginHost } from 'aws-cdk/lib/plugin'; +import { Command } from 'aws-cdk/lib/settings'; +import * as AWS from 'aws-sdk'; +import * as fs from 'fs'; + +import { AccountsConfig, GlobalConfig } from '@aws-accelerator/config'; +import { throttlingBackOff } from '@aws-accelerator/utils'; +import { AssumeProfilePlugin } from '@aws-cdk-extensions/cdk-plugin-assume-role'; + +import { AcceleratorStage } from './accelerator-stage'; +import { Logger } from './logger'; +import { AcceleratorToolkit } from './toolkit'; + +/** + * S3 server access logs bucket name prefix. + */ +export const S3ServerAccessLogsBucketNamePrefix = 'aws-accelerator-s3-access-logs'; + +/** + * List of AWS ELB root account and regions mapping + */ +export const AcceleratorElbRootAccounts: Record = { + 'us-east-1': '127311923021', + 'us-east-2': '033677994240', + 'us-west-1': '027434742980', + 'us-west-2': '797873946194', + 'af-south-1': '098369216593', + 'ca-central-1': '985666609251', + 'eu-central-1': '054676820928', + 'eu-west-1': '156460612806', + 'eu-west-2': '652711504416', + 'eu-south-1': '635631232127', + 'eu-west-3': '009996457667', + 'eu-north-1': '897822967062', + 'ap-east-1': '754344448648', + 'ap-northeast-1': '582318560864', + 'ap-northeast-2': '600734575887', + 'ap-northeast-3': '383597477331', + 'ap-southeast-1': '114774131450', + 'ap-southeast-2': '783225319266', + 'ap-southeast-3': '589379963580', + 'ap-south-1': '718504428378', + 'me-south-1': '076674570225', + 'sa-east-1': '507241528517', + 'us-gov-west-1': '048591011584', + 'us-gov-east-1': '190560391635', + 'cn-north-1': '638102146993', + 'cn-northwest-1': '037604701340', +}; + +/** + * constant maintaining cloudformation stack names + */ +export const AcceleratorStackNames: Record = { + [AcceleratorStage.PREPARE]: 'AWSAccelerator-PrepareStack', + [AcceleratorStage.PIPELINE]: 'AWSAccelerator-PipelineStack', + [AcceleratorStage.TESTER_PIPELINE]: 'AWSAccelerator-TesterPipelineStack', + [AcceleratorStage.ORGANIZATIONS]: 'AWSAccelerator-OrganizationsStack', + [AcceleratorStage.KEY]: 'AWSAccelerator-KeyStack', + [AcceleratorStage.LOGGING]: 'AWSAccelerator-LoggingStack', + [AcceleratorStage.ACCOUNTS]: 'AWSAccelerator-AccountsStack', + [AcceleratorStage.DEPENDENCIES]: 'AWSAccelerator-DependenciesStack', + [AcceleratorStage.SECURITY]: 'AWSAccelerator-SecurityStack', + [AcceleratorStage.SECURITY_RESOURCES]: 'AWSAccelerator-SecurityResourcesStack', + [AcceleratorStage.OPERATIONS]: 'AWSAccelerator-OperationsStack', + [AcceleratorStage.NETWORK_PREP]: 'AWSAccelerator-NetworkPrepStack', + [AcceleratorStage.NETWORK_VPC]: 'AWSAccelerator-NetworkVpcStack', + [AcceleratorStage.NETWORK_VPC_ENDPOINTS]: 'AWSAccelerator-NetworkVpcEndpointsStack', + [AcceleratorStage.NETWORK_VPC_DNS]: 'AWSAccelerator-NetworkVpcDnsStack', + [AcceleratorStage.NETWORK_ASSOCIATIONS]: 'AWSAccelerator-NetworkAssociationsStack', + [AcceleratorStage.FINALIZE]: 'AWSAccelerator-FinalizeStack', + [AcceleratorStage.SECURITY_AUDIT]: 'AWSAccelerator-SecurityAuditStack', +}; + +/** + * + */ +export interface AcceleratorProps { + readonly command: string; + readonly configDirPath: string; + readonly stage?: string; + readonly account?: string; + readonly region?: string; + readonly partition: string; + readonly requireApproval: RequireApproval; + readonly app?: string; +} + +/** + * Wrapper around the CdkToolkit. The Accelerator defines this wrapper to add + * the following functionality: + * + * - x + * - y + * - z + */ +export abstract class Accelerator { + // private static readonly DEFAULT_MAX_CONCURRENT_STACKS = 20; + + static isSupportedStage(stage: AcceleratorStage): boolean { + if (stage === undefined) { + return false; + } + return Object.values(AcceleratorStage).includes(stage); + } + + /** + * + * @returns + */ + static async run(props: AcceleratorProps): Promise { + let managementAccountCredentials = undefined; + let globalConfig = undefined; + let assumeRolePlugin = undefined; + + if (props.stage !== AcceleratorStage.PIPELINE) { + // Get management account credential when pipeline is executing outside of management account + managementAccountCredentials = await this.getManagementAccountCredentials(props.partition); + + // Load in the global config to read in the management account access roles + globalConfig = GlobalConfig.load(props.configDirPath); + + // + // Load Plugins + // + assumeRolePlugin = new AssumeProfilePlugin({ + assumeRoleName: globalConfig.managementAccountAccessRole, + assumeRoleDuration: 3600, + credentials: managementAccountCredentials, + partition: props.partition, + }); + assumeRolePlugin.init(PluginHost.instance); + } + + // + // When an account and region is specified, execute as single stack + // + if (props.account || props.region) { + if (props.account && props.region === undefined) { + throw new Error(`Account set to ${props.account}, but region is undefined`); + } + if (props.region && props.account === undefined) { + throw new Error(`Region set to ${props.region}, but region is undefined`); + } + + return await AcceleratorToolkit.execute({ + command: props.command, + accountId: props.account, + region: props.region, + partition: props.partition, + stage: props.stage, + configDirPath: props.configDirPath, + requireApproval: props.requireApproval, + app: props.app, + }); + } + + // Treat synthesize as a single - do not need parallel paths to generate all stacks + if (props.command === Command.SYNTH || props.command === Command.SYNTHESIZE || props.command === Command.DIFF) { + return await AcceleratorToolkit.execute({ + command: props.command, + accountId: props.account, + region: props.region, + partition: props.partition, + stage: props.stage, + configDirPath: props.configDirPath, + requireApproval: props.requireApproval, + app: props.app, + }); + } + + // + // Read in all Accelerator Configuration files here, then pass the objects + // to the stacks that need them. Exceptions are thrown if any of the + // configuration files are malformed. + // + globalConfig = GlobalConfig.load(props.configDirPath); + const accountsConfig = AccountsConfig.load(props.configDirPath); + + // + // Will load in account IDs using the Organizations client if not provided + // as inputs in accountsConfig + // + await accountsConfig.loadAccountIds(props.partition); + + // + // When running parallel, this will be the max concurrent stacks + // + const maxStacks = process.env['MAX_CONCURRENT_STACKS'] ?? 500; + + const promises: Promise[] = []; + + // + // Execute Bootstrap stacks for all identified accounts + // + if (props.command == 'bootstrap') { + const trustedAccountId = accountsConfig.getManagementAccountId(); + for (const region of globalConfig.enabledRegions) { + for (const account of [...accountsConfig.mandatoryAccounts, ...accountsConfig.workloadAccounts]) { + promises.push( + AcceleratorToolkit.execute({ + command: props.command, + accountId: accountsConfig.getAccountId(account.name), + region, + partition: props.partition, + trustedAccountId, + configDirPath: props.configDirPath, + requireApproval: props.requireApproval, + app: props.app, + }), + ); + + if (promises.length >= maxStacks) { + await Promise.all(promises); + } + } + } + await Promise.all(promises); + return; + } + + let globalRegion = 'us-east-1'; + + if (props.partition === 'aws-us-gov') { + globalRegion = 'us-gov-west-1'; + } + // Control Tower: To start a well-planned OU structure in your landing zone, AWS Control Tower + // sets up a Security OU for you. This OU contains three shared accounts: the management + // (primary) account, the log archive account, and the security audit account (also referred to + // as the audit account). + if (props.stage === AcceleratorStage.ACCOUNTS) { + Logger.info(`[accelerator] Executing ${props.stage} for Management account.`); + await AcceleratorToolkit.execute({ + command: props.command, + accountId: accountsConfig.getManagementAccountId(), + region: globalRegion, + partition: props.partition, + stage: props.stage, + configDirPath: props.configDirPath, + requireApproval: props.requireApproval, + app: props.app, + }); + } + + if (props.stage === AcceleratorStage.PREPARE) { + Logger.info(`[accelerator] Executing ${props.stage} for Management account.`); + await AcceleratorToolkit.execute({ + command: props.command, + accountId: accountsConfig.getManagementAccountId(), + region: globalConfig.homeRegion, + partition: props.partition, + stage: props.stage, + configDirPath: props.configDirPath, + requireApproval: props.requireApproval, + app: props.app, + }); + } + + if (props.stage === AcceleratorStage.FINALIZE) { + Logger.info(`[accelerator] Executing ${props.stage} for Management account.`); + await AcceleratorToolkit.execute({ + command: props.command, + accountId: accountsConfig.getManagementAccountId(), + region: globalRegion, + partition: props.partition, + stage: props.stage, + configDirPath: props.configDirPath, + requireApproval: props.requireApproval, + app: props.app, + }); + } + + if (props.stage === AcceleratorStage.ORGANIZATIONS) { + for (const region of globalConfig.enabledRegions) { + Logger.info(`[accelerator] Executing ${props.stage} for Management account in ${region} region.`); + await delay(1000); + promises.push( + AcceleratorToolkit.execute({ + command: props.command, + accountId: accountsConfig.getManagementAccountId(), + region: region, + partition: props.partition, + stage: props.stage, + configDirPath: props.configDirPath, + requireApproval: props.requireApproval, + app: props.app, + }), + ); + if (promises.length >= maxStacks) { + await Promise.all(promises); + } + } + } + + if (props.stage === AcceleratorStage.KEY || props.stage === AcceleratorStage.SECURITY_AUDIT) { + for (const region of globalConfig.enabledRegions) { + Logger.info(`[accelerator] Executing ${props.stage} for audit account in ${region} region.`); + await delay(1000); + promises.push( + AcceleratorToolkit.execute({ + command: props.command, + accountId: accountsConfig.getAuditAccountId(), + region: region, + partition: props.partition, + stage: props.stage, + configDirPath: props.configDirPath, + requireApproval: props.requireApproval, + app: props.app, + }), + ); + if (promises.length >= maxStacks) { + await Promise.all(promises); + } + } + } + + if ( + props.stage === AcceleratorStage.LOGGING || + props.stage === AcceleratorStage.SECURITY || + props.stage === AcceleratorStage.SECURITY_RESOURCES || + props.stage === AcceleratorStage.OPERATIONS || + props.stage === AcceleratorStage.NETWORK_PREP || + props.stage === AcceleratorStage.NETWORK_VPC || + props.stage === AcceleratorStage.NETWORK_ASSOCIATIONS + ) { + for (const region of globalConfig.enabledRegions) { + for (const account of [...accountsConfig.mandatoryAccounts, ...accountsConfig.workloadAccounts]) { + Logger.info(`[accelerator] Executing ${props.stage} for ${account.name} account in ${region} region.`); + await delay(1000); + promises.push( + AcceleratorToolkit.execute({ + command: props.command, + accountId: accountsConfig.getAccountId(account.name), + region, + partition: props.partition, + stage: props.stage, + configDirPath: props.configDirPath, + requireApproval: props.requireApproval, + app: props.app, + }), + ); + if (promises.length >= maxStacks) { + await Promise.all(promises); + } + } + } + } + + await Promise.all(promises); + } + + static async getManagementAccountCredentials(partition: string): Promise { + if (process.env['CREDENTIALS_PATH'] && fs.existsSync(process.env['CREDENTIALS_PATH'])) { + Logger.info('Detected Debugging environment. Loading temporary credentials.'); + + const credentialsString = fs.readFileSync(process.env['CREDENTIALS_PATH']).toString(); + const credentials = JSON.parse(credentialsString); + + // Support for V2 SDK + AWS.config.update({ + accessKeyId: credentials.AccessKeyId, + secretAccessKey: credentials.SecretAccessKey, + sessionToken: credentials.SessionToken, + }); + } + if ( + process.env['MANAGEMENT_ACCOUNT_ID'] && + process.env['MANAGEMENT_ACCOUNT_ROLE_NAME'] && + process.env['ACCOUNT_ID'] !== process.env['MANAGEMENT_ACCOUNT_ID'] + ) { + Logger.info('[accelerator] set management account credentials'); + Logger.info(`[accelerator] managementAccountId => ${process.env['MANAGEMENT_ACCOUNT_ID']}`); + Logger.info(`[accelerator] management account role name => ${process.env['MANAGEMENT_ACCOUNT_ROLE_NAME']}`); + + const roleArn = `arn:${partition}:iam::${process.env['MANAGEMENT_ACCOUNT_ID']}:role/${process.env['MANAGEMENT_ACCOUNT_ROLE_NAME']}`; + const stsClient = new AWS.STS({ region: process.env['AWS_REGION'] }); + Logger.info(`[accelerator] management account roleArn => ${roleArn}`); + + const assumeRoleCredential = await throttlingBackOff(() => + stsClient.assumeRole({ RoleArn: roleArn, RoleSessionName: 'acceleratorAssumeRoleSession' }).promise(), + ); + + process.env['AWS_ACCESS_KEY_ID'] = assumeRoleCredential.Credentials!.AccessKeyId!; + process.env['AWS_ACCESS_KEY'] = assumeRoleCredential.Credentials!.AccessKeyId!; + process.env['AWS_SECRET_KEY'] = assumeRoleCredential.Credentials!.SecretAccessKey!; + process.env['AWS_SECRET_ACCESS_KEY'] = assumeRoleCredential.Credentials!.SecretAccessKey!; + process.env['AWS_SESSION_TOKEN'] = assumeRoleCredential.Credentials!.SessionToken; + + // Support for V2 SDK + AWS.config.update({ + accessKeyId: assumeRoleCredential.Credentials!.AccessKeyId, + secretAccessKey: assumeRoleCredential.Credentials!.SecretAccessKey, + sessionToken: assumeRoleCredential.Credentials!.SessionToken, + }); + + return assumeRoleCredential.Credentials; + } else { + return undefined; + } + } +} + +function delay(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} diff --git a/source/packages/@aws-accelerator/accelerator/lib/config-repository.ts b/source/packages/@aws-accelerator/accelerator/lib/config-repository.ts new file mode 100644 index 000000000..f02839a4e --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/lib/config-repository.ts @@ -0,0 +1,98 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { + AccountsConfig, + GlobalConfig, + IamConfig, + NetworkConfig, + OrganizationConfig, + SecurityConfig, + Region, +} from '@aws-accelerator/config'; +import * as cdk_extensions from '@aws-cdk-extensions/cdk-extensions'; +import * as cdk from 'aws-cdk-lib'; +import * as s3_assets from 'aws-cdk-lib/aws-s3-assets'; +import { Construct } from 'constructs'; +import * as fs from 'fs'; +import * as yaml from 'js-yaml'; +import * as os from 'os'; +import * as path from 'path'; + +export interface ConfigRepositoryProps { + readonly repositoryName: string; + readonly repositoryBranchName?: string; + readonly description?: string; + readonly managementAccountEmail: string; + readonly logArchiveAccountEmail: string; + readonly auditAccountEmail: string; +} + +/** + * Class to create AWS accelerator configuration repository and initialize the repository with default configuration + */ +export class ConfigRepository extends Construct { + readonly configRepo: cdk_extensions.Repository; + + constructor(scope: Construct, id: string, props: ConfigRepositoryProps) { + super(scope, id); + + // + // Generate default configuration files + // + const tempDirPath = fs.mkdtempSync(path.join(os.tmpdir(), 'config-assets-')); + + fs.writeFileSync( + path.join(tempDirPath, AccountsConfig.FILENAME), + yaml.dump( + new AccountsConfig({ + managementAccountEmail: props.managementAccountEmail, + logArchiveAccountEmail: props.logArchiveAccountEmail, + auditAccountEmail: props.auditAccountEmail, + }), + ), + 'utf8', + ); + fs.writeFileSync( + path.join(tempDirPath, GlobalConfig.FILENAME), + yaml.dump(new GlobalConfig({ homeRegion: cdk.Stack.of(this).region as Region })), + 'utf8', + ); + fs.writeFileSync(path.join(tempDirPath, IamConfig.FILENAME), yaml.dump(new IamConfig()), 'utf8'); + fs.writeFileSync(path.join(tempDirPath, NetworkConfig.FILENAME), yaml.dump(new NetworkConfig()), 'utf8'); + fs.writeFileSync(path.join(tempDirPath, OrganizationConfig.FILENAME), yaml.dump(new OrganizationConfig()), 'utf8'); + fs.writeFileSync(path.join(tempDirPath, SecurityConfig.FILENAME), yaml.dump(new SecurityConfig()), 'utf8'); + + const configurationDefaultsAssets = new s3_assets.Asset(this, 'ConfigurationDefaultsAssets', { + path: tempDirPath, + }); + + this.configRepo = new cdk_extensions.Repository(this, 'Resource', { + repositoryName: props.repositoryName, + repositoryBranchName: props.repositoryBranchName!, + s3BucketName: configurationDefaultsAssets.bucket.bucketName, + s3key: configurationDefaultsAssets.s3ObjectKey, + }); + + // TODO: Add delete protection on the CodeCommit repository + } + + /** + * Method to get initialized repository object + * + * @return Returns Initialized repository object. + */ + public getRepository(): cdk_extensions.Repository { + return this.configRepo; + } +} diff --git a/source/packages/@aws-accelerator/accelerator/lib/detach-quarantine-scp.ts b/source/packages/@aws-accelerator/accelerator/lib/detach-quarantine-scp.ts new file mode 100644 index 000000000..7985e9b5a --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/lib/detach-quarantine-scp.ts @@ -0,0 +1,102 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { v4 as uuidv4 } from 'uuid'; +import { Construct } from 'constructs'; +import { Duration } from 'aws-cdk-lib'; +import path = require('path'); + +export interface DetachQuarantineScpProps { + readonly scpPolicyId: string; + readonly partition: string; + readonly managementAccountId: string; + /** + * Custom resource lambda log group encryption key + */ + readonly kmsKey: cdk.aws_kms.Key; + /** + * Custom resource lambda log retention in days + */ + readonly logRetentionInDays: number; +} + +/** + * Class Detach Quarantine SCP + */ +export class DetachQuarantineScp extends Construct { + public readonly id: string; + + constructor(scope: Construct, id: string, props: DetachQuarantineScpProps) { + super(scope, id); + + const DETACH_QUARANTINE_SCP_RESOURCE_TYPE = 'Custom::DetachQuarantineScp'; + + // + // Function definition for the custom resource + // + const provider = cdk.CustomResourceProvider.getOrCreateProvider(this, DETACH_QUARANTINE_SCP_RESOURCE_TYPE, { + codeDirectory: path.join(__dirname, 'lambdas/detach-quarantine-scp/dist'), + runtime: cdk.CustomResourceProviderRuntime.NODEJS_14_X, + timeout: Duration.minutes(15), + policyStatements: [ + { + Sid: 'organizations', + Effect: 'Allow', + Action: ['organizations:ListAccounts'], + Resource: '*', + }, + { + Sid: 'detach', + Effect: 'Allow', + Action: ['organizations:DetachPolicy'], + Resource: [ + `arn:${props.partition}:organizations::${props.managementAccountId}:policy/o-*/service_control_policy/${props.scpPolicyId}`, + `arn:${props.partition}:organizations::${props.managementAccountId}:account/o-*/*`, + ], + }, + ], + }); + + // + // Custom Resource definition. We want this resource to be evaluated on + // every CloudFormation update, so we generate a new uuid to force + // re-evaluation. + // + const resource = new cdk.CustomResource(this, 'Resource', { + resourceType: DETACH_QUARANTINE_SCP_RESOURCE_TYPE, + serviceToken: provider.serviceToken, + properties: { + scpPolicyId: props.scpPolicyId, + uuid: uuidv4(), // Generates a new UUID to force the resource to update + }, + }); + + /** + * Singleton pattern to define the log group for the singleton function + * in the stack + */ + const stack = cdk.Stack.of(scope); + const logGroup = + (stack.node.tryFindChild(`${provider.node.id}LogGroup`) as cdk.aws_logs.LogGroup) ?? + new cdk.aws_logs.LogGroup(stack, `${provider.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${(provider.node.findChild('Handler') as cdk.aws_lambda.CfnFunction).ref}`, + retention: props.logRetentionInDays, + encryptionKey: props.kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + resource.node.addDependency(logGroup); + + this.id = resource.ref; + } +} diff --git a/source/packages/@aws-accelerator/accelerator/lib/lambdas/attach-quarantine-scp/index.ts b/source/packages/@aws-accelerator/accelerator/lib/lambdas/attach-quarantine-scp/index.ts new file mode 100644 index 000000000..a1d602a09 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/lib/lambdas/attach-quarantine-scp/index.ts @@ -0,0 +1,114 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as AWS from 'aws-sdk'; +import { delay, throttlingBackOff } from '@aws-accelerator/utils'; + +const organizationsClient = new AWS.Organizations({ region: 'us-east-1' }); + +/** + * attach-quarantine-scp - lambda handler + * + * @param event + * @returns + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export async function handler(event: any): Promise { + console.log(event); + + // eslint-disable-next-line prefer-const + const policyName: string = process.env['SCP_POLICY_NAME'] ?? ''; + + const createAccountStatus = JSON.parse(JSON.stringify(event.detail.responseElements.createAccountStatus)); + console.log(`Create Account Request Id: ${createAccountStatus.id}`); + + let statusResponse = await getAccountCreationStatus(createAccountStatus.id); + while (statusResponse.CreateAccountStatus?.State !== 'SUCCEEDED') { + await delay(1000); + statusResponse = await getAccountCreationStatus(createAccountStatus.id); + } + if (statusResponse.CreateAccountStatus?.State === 'SUCCEEDED') { + const policyId = await getPolicyId(policyName); + if (policyId === 'NotFound') { + console.error( + `Policy with name ${policyName} was not found. Policy was not applied to account ${statusResponse.CreateAccountStatus.AccountId}`, + ); + return { + statusCode: 200, + }; + } + await attachQuarantineScp(statusResponse.CreateAccountStatus.AccountId ?? '', policyId ?? ''); + } + console.log(statusResponse); + return { + statusCode: 200, + }; +} + +async function getAccountCreationStatus( + requestId: string, +): Promise { + const response = await throttlingBackOff(() => + organizationsClient.describeCreateAccountStatus({ CreateAccountRequestId: requestId }).promise(), + ); + return response; +} + +async function getPolicyId(policyName: string) { + console.log(`Looking for policy named ${policyName}`); + const scpPolicies: AWS.Organizations.PolicySummary[] = []; + let nextToken: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => + organizationsClient.listPolicies({ Filter: 'SERVICE_CONTROL_POLICY' }).promise(), + ); + + for (const scpPolicy of page.Policies ?? []) { + console.log(`Policy named ${scpPolicy.Name} added to list`); + scpPolicies.push(scpPolicy); + } + nextToken = page.NextToken; + } while (nextToken); + + const policy = scpPolicies.find(item => item.Name === policyName); + if (policy) { + console.log(policy); + return policy?.Id; + } else { + return 'NotFound'; + } +} + +async function attachQuarantineScp(accountId: string, policyId: string): Promise { + try { + await throttlingBackOff(() => + organizationsClient + .attachPolicy({ + PolicyId: policyId, + TargetId: accountId, + }) + .promise(), + ); + console.log(`Attached Quarantine SCP to account: ${accountId}`); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (e: any) { + if (e.code === 'DuplicatePolicyAttachmentException') { + console.log(`Quarantine SCP was previously attached to account: ${accountId}`); + return true; + } else { + console.log(e); + return false; + } + } + return true; +} diff --git a/source/packages/@aws-accelerator/accelerator/lib/lambdas/attach-quarantine-scp/package.json b/source/packages/@aws-accelerator/accelerator/lib/lambdas/attach-quarantine-scp/package.json new file mode 100644 index 000000000..aa10ef7b3 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/lib/lambdas/attach-quarantine-scp/package.json @@ -0,0 +1,46 @@ +{ + "name": "@aws-accelerator/lambdas-attach-quarantine-scp", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --sourcemap --platform=node --target=node14 --external:aws-sdk --outfile=./dist/index.js index.ts", + "test": "", + "testreport": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2", + "ts-node": "10.7.0" + }, + "peerDependencies": { + "eslint": "8.4.1" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1001.0" + } +} diff --git a/source/packages/@aws-accelerator/accelerator/lib/lambdas/attach-quarantine-scp/tsconfig.json b/source/packages/@aws-accelerator/accelerator/lib/lambdas/attach-quarantine-scp/tsconfig.json new file mode 100644 index 000000000..ebcb98a79 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/lib/lambdas/attach-quarantine-scp/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} diff --git a/source/packages/@aws-accelerator/accelerator/lib/lambdas/detach-quarantine-scp/index.ts b/source/packages/@aws-accelerator/accelerator/lib/lambdas/detach-quarantine-scp/index.ts new file mode 100644 index 000000000..138b8fbfc --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/lib/lambdas/detach-quarantine-scp/index.ts @@ -0,0 +1,93 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as AWS from 'aws-sdk'; +import { throttlingBackOff } from '@aws-accelerator/utils'; + +const organizationsClient = new AWS.Organizations(); + +/** + * detach-quarantine-scp - lambda handler + * + * @param event + * @returns + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + PhysicalResourceId: string | undefined; + Status: string; + } + | undefined +> { + console.log(event); + const policyId: string = event.ResourceProperties['scpPolicyId'] ?? ''; + + switch (event.RequestType) { + case 'Create': + case 'Update': + const accountIdList = await getAccountIds(); + + for (const accountId of accountIdList) { + await detachQuarantineScp(accountId ?? '', policyId ?? ''); + } + return { + PhysicalResourceId: 'detach-quarantine-scp', + Status: 'SUCCESS', + }; + case 'Delete': + return { + PhysicalResourceId: event.PhysicalResourceId, + Status: 'SUCCESS', + }; + } +} + +async function getAccountIds(): Promise { + console.log('Getting list of accounts'); + const accountIdList: string[] = []; + let nextToken: string | undefined = undefined; + do { + const params: AWS.Organizations.ListAccountsRequest = { NextToken: nextToken }; + const page = await throttlingBackOff(() => organizationsClient.listAccounts(params).promise()); + for (const account of page.Accounts ?? []) { + accountIdList.push(account.Id!); + } + nextToken = page.NextToken; + } while (nextToken); + return accountIdList; +} + +async function detachQuarantineScp(accountId: string, policyId: string): Promise { + try { + await throttlingBackOff(() => + organizationsClient + .detachPolicy({ + PolicyId: policyId, + TargetId: accountId, + }) + .promise(), + ); + console.log(`Detached Quarantine SCP from account: ${accountId}`); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (e: any) { + if (e.code === 'PolicyNotAttachedException') { + //console.log(`Quarantine SCP was not attached to account: ${accountId}`); + return true; + } else { + console.log(e); + return false; + } + } + return true; +} diff --git a/source/packages/@aws-accelerator/accelerator/lib/lambdas/detach-quarantine-scp/package.json b/source/packages/@aws-accelerator/accelerator/lib/lambdas/detach-quarantine-scp/package.json new file mode 100644 index 000000000..3841c0e8b --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/lib/lambdas/detach-quarantine-scp/package.json @@ -0,0 +1,46 @@ +{ + "name": "@aws-accelerator/lambdas-detach-quarantine-scp", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --sourcemap --platform=node --target=node14 --external:aws-sdk --outfile=./dist/index.js index.ts", + "test": "", + "testreport": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2", + "ts-node": "10.7.0" + }, + "peerDependencies": { + "eslint": "8.4.1" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1001.0" + } +} diff --git a/source/packages/@aws-accelerator/accelerator/lib/lambdas/detach-quarantine-scp/tsconfig.json b/source/packages/@aws-accelerator/accelerator/lib/lambdas/detach-quarantine-scp/tsconfig.json new file mode 100644 index 000000000..ebcb98a79 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/lib/lambdas/detach-quarantine-scp/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} diff --git a/source/packages/@aws-accelerator/accelerator/lib/lambdas/load-config-table/index.ts b/source/packages/@aws-accelerator/accelerator/lib/lambdas/load-config-table/index.ts new file mode 100644 index 000000000..e9ab813e5 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/lib/lambdas/load-config-table/index.ts @@ -0,0 +1,321 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; +import { DynamoDBDocumentClient, UpdateCommand, UpdateCommandInput } from '@aws-sdk/lib-dynamodb'; +import { CloudFormationClient, DescribeStacksCommand } from '@aws-sdk/client-cloudformation'; +import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'; +import { throttlingBackOff } from '@aws-accelerator/utils'; +import * as yaml from 'js-yaml'; +import { + AccountConfig, + AccountsConfig, + AccountsConfigTypes, + OrganizationalUnitConfig, + OrganizationConfig, + OrganizationConfigTypes, +} from '@aws-accelerator/config'; +import * as t from '@aws-accelerator/config/'; +import { Readable } from 'stream'; + +export {}; +declare global { + type File = unknown; + type ReadableStream = unknown; +} + +const dynamodbClient = new DynamoDBClient({}); +const documentClient = DynamoDBDocumentClient.from(dynamodbClient); +const cloudformationClient = new CloudFormationClient({}); +const s3Client = new S3Client({}); + +/** + * load-config-table - lambda handler + * + * @param event + * @returns + */ +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + PhysicalResourceId: string | undefined; + Status: string; + } + | undefined +> { + console.log(event); + const configTableName: string = event.ResourceProperties['configTableName'] ?? ''; + const configRepositoryName: string = event.ResourceProperties['configRepositoryName' ?? '']; + const managementAccountEmail: string = event.ResourceProperties['managementAccountEmail' ?? '']; + const auditAccountEmail: string = event.ResourceProperties['auditAccountEmail' ?? '']; + const logArchiveAccountEmail: string = event.ResourceProperties['logArchiveAccountEmail' ?? '']; + const configS3Bucket: string = event.ResourceProperties['configS3Bucket' ?? '']; + const organizationsConfigS3Key: string = event.ResourceProperties['organizationsConfigS3Key' ?? '']; + const accountConfigS3Key: string = event.ResourceProperties['accountConfigS3Key' ?? '']; + const commitId: string = event.ResourceProperties['commitId' ?? '']; + const partition = event.ResourceProperties['partition']; + const stackName = event.ResourceProperties['stackName']; + + console.log(`Configuration Table Name: ${configTableName}`); + console.log(`Configuration Repository Name: ${configRepositoryName}`); + + switch (event.RequestType) { + case 'Create': + case 'Update': + console.log(stackName); + // if stack rollback is in progress don't do anything + // the stack may have failed as the results of errors + // from this construct + // when rolling back this construct will execute and + // fail again preventing stack rollback + if (await isStackInRollback(stackName)) { + console.log('Stack in rollback exiting'); + return { + PhysicalResourceId: 'loadConfigTableNone', + Status: 'SUCCESS', + }; + } + const organizationConfigContent = await getConfigFileContents(configS3Bucket, organizationsConfigS3Key); + const organizationValues = t.parse( + OrganizationConfigTypes.organizationConfig, + yaml.load(organizationConfigContent), + ); + const organizationConfig = new OrganizationConfig(organizationValues); + await organizationConfig.loadOrganizationalUnitIds(partition); + for (const organizationalUnit of organizationConfig.organizationalUnits) { + let awsKey = ''; + try { + awsKey = organizationConfig.getOrganizationalUnitId(organizationalUnit.name); + } catch (error) { + let message; + + if (error instanceof Error) message = error.message; + else message = String(error); + + if (message.startsWith('Organizations not enabled or')) awsKey = ''; + else throw error; + } + await putOrganizationConfigInTable(organizationalUnit, configTableName, awsKey, commitId); + } + const accountsConfigContent = await getConfigFileContents(configS3Bucket, accountConfigS3Key); + const accountsValues = t.parse(AccountsConfigTypes.accountsConfig, yaml.load(accountsConfigContent)); + const accountsConfig = new AccountsConfig( + { + managementAccountEmail: managementAccountEmail, + auditAccountEmail: auditAccountEmail, + logArchiveAccountEmail: logArchiveAccountEmail, + }, + accountsValues, + ); + await accountsConfig.loadAccountIds(partition); + for (const account of accountsConfig.mandatoryAccounts) { + switch (account.name) { + case 'Management': + const managmentId = accountsConfig.getManagementAccountId(); + await putAccountConfigInTable( + 'mandatory', + account, + configTableName, + managmentId, + commitId, + account.organizationalUnit, + ); + break; + case 'LogArchive': + const logArchiveId = accountsConfig.getLogArchiveAccountId(); + await putAccountConfigInTable( + 'mandatory', + account, + configTableName, + logArchiveId, + commitId, + account.organizationalUnit, + ); + break; + case 'Audit': + const auditId = accountsConfig.getAuditAccountId(); + await putAccountConfigInTable( + 'mandatory', + account, + configTableName, + auditId, + commitId, + account.organizationalUnit, + ); + break; + } + const awsKey = accountsConfig.getAccountId(account.name) || ''; + await putAccountConfigInTable( + 'mandatory', + account, + configTableName, + awsKey, + commitId, + account.organizationalUnit, + ); + } + for (const account of accountsConfig.workloadAccounts) { + let accountId: string; + try { + accountId = accountsConfig.getAccountId(account.name); + } catch { + accountId = ''; + } + await putAccountConfigInTable( + 'workload', + account, + configTableName, + accountId, + commitId, + account.organizationalUnit, + ); + } + return { + PhysicalResourceId: commitId, + Status: 'Success', + }; + case 'Delete': + return { + PhysicalResourceId: event.PhysicalResourceId, + Status: 'Success', + }; + } +} + +async function getConfigFileContents(configFileS3Bucket: string, configFileS3Key: string): Promise { + const response = await throttlingBackOff(() => + s3Client.send(new GetObjectCommand({ Bucket: configFileS3Bucket, Key: configFileS3Key })), + ); + const stream = response.Body as Readable; + const contents = await streamToString(stream); + return contents; +} + +async function streamToString(stream: Readable): Promise { + return await new Promise((resolve, reject) => { + const chunks: Uint8Array[] = []; + stream.on('data', chunk => chunks.push(chunk)); + stream.on('error', reject); + stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8'))); + }); +} + +async function putOrganizationConfigInTable( + configData: OrganizationalUnitConfig, + configTableName: string, + awsKey: string, + commitId: string, +): Promise { + if (awsKey != '') { + const params: UpdateCommandInput = { + TableName: configTableName, + Key: { + dataType: 'organization', + acceleratorKey: configData.name, + }, + UpdateExpression: 'set #awsKey = :v_awsKey, #dataBag = :v_dataBag, #commitId = :v_commitId', + ExpressionAttributeNames: { + '#awsKey': 'awsKey', + '#dataBag': 'dataBag', + '#commitId': 'commitId', + }, + ExpressionAttributeValues: { + ':v_awsKey': awsKey, + ':v_dataBag': JSON.stringify(configData), + ':v_commitId': commitId, + }, + }; + await throttlingBackOff(() => documentClient.send(new UpdateCommand(params))); + } else { + const params: UpdateCommandInput = { + TableName: configTableName, + Key: { + dataType: 'organization', + acceleratorKey: configData.name, + }, + UpdateExpression: 'set #dataBag = :v_dataBag, #commitId = :v_commitId', + ExpressionAttributeNames: { + '#dataBag': 'dataBag', + '#commitId': 'commitId', + }, + ExpressionAttributeValues: { + ':v_dataBag': JSON.stringify(configData), + ':v_commitId': commitId, + }, + }; + await throttlingBackOff(() => documentClient.send(new UpdateCommand(params))); + } +} + +async function putAccountConfigInTable( + accountType: string, + configData: AccountConfig, + configTableName: string, + awsKey: string, + commitId: string, + ouName: string, +): Promise { + if (awsKey !== '') { + const params: UpdateCommandInput = { + TableName: configTableName, + Key: { + dataType: accountType + 'Account', + acceleratorKey: configData.email, + }, + UpdateExpression: 'set #awsKey = :v_awsKey, #dataBag = :v_dataBag, #commitId = :v_commitId, #ouName = :v_ouName', + ExpressionAttributeNames: { + '#awsKey': 'awsKey', + '#dataBag': 'dataBag', + '#commitId': 'commitId', + '#ouName': 'ouName', + }, + ExpressionAttributeValues: { + ':v_awsKey': awsKey, + ':v_dataBag': JSON.stringify(configData), + ':v_commitId': commitId, + ':v_ouName': ouName, + }, + }; + await throttlingBackOff(() => documentClient.send(new UpdateCommand(params))); + } else { + const params: UpdateCommandInput = { + TableName: configTableName, + Key: { + dataType: accountType + 'Account', + acceleratorKey: configData.email, + }, + UpdateExpression: 'set #dataBag = :v_dataBag, #commitId = :v_commitId, #ouName = :v_ouName', + ExpressionAttributeNames: { + '#dataBag': 'dataBag', + '#commitId': 'commitId', + '#ouName': 'ouName', + }, + ExpressionAttributeValues: { + ':v_dataBag': JSON.stringify(configData), + ':v_commitId': commitId, + ':v_ouName': ouName, + }, + }; + await throttlingBackOff(() => documentClient.send(new UpdateCommand(params))); + } +} + +async function isStackInRollback(stackName: string): Promise { + const response = await throttlingBackOff(() => + cloudformationClient.send(new DescribeStacksCommand({ StackName: stackName })), + ); + console.log(response); + if (response.Stacks && response.Stacks[0].StackStatus == 'UPDATE_ROLLBACK_IN_PROGRESS') { + return true; + } + return false; +} diff --git a/source/packages/@aws-accelerator/accelerator/lib/lambdas/load-config-table/package.json b/source/packages/@aws-accelerator/accelerator/lib/lambdas/load-config-table/package.json new file mode 100644 index 000000000..aebbbd7e7 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/lib/lambdas/load-config-table/package.json @@ -0,0 +1,52 @@ +{ + "name": "@aws-accelerator/load-config-table", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --sourcemap --platform=node --target=node14 --external:aws-sdk --outfile=./dist/index.js index.ts", + "test": "", + "testreport": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2", + "ts-node": "10.7.0" + }, + "peerDependencies": { + "eslint": "8.4.1" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "@aws-accelerator/config": "^0.0.0", + "t": "file:../../../../config/lib/common-types", + "@aws-sdk/client-dynamodb": "3.92.0", + "@aws-sdk/lib-dynamodb": "3.92.0", + "@aws-sdk/client-cloudformation": "3.92.0", + "@aws-sdk/client-s3": "3.92.0", + "js-yaml": "4.1.0" + } +} diff --git a/source/packages/@aws-accelerator/accelerator/lib/lambdas/load-config-table/tsconfig.json b/source/packages/@aws-accelerator/accelerator/lib/lambdas/load-config-table/tsconfig.json new file mode 100644 index 000000000..88a8b3636 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/lib/lambdas/load-config-table/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "moduleResolution": "node", + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} diff --git a/source/packages/@aws-accelerator/accelerator/lib/lambdas/validate-environment/index.ts b/source/packages/@aws-accelerator/accelerator/lib/lambdas/validate-environment/index.ts new file mode 100644 index 000000000..944d0bc26 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/lib/lambdas/validate-environment/index.ts @@ -0,0 +1,511 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as AWS from 'aws-sdk'; +import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; +import { + DynamoDBDocumentClient, + PutCommand, + PutCommandInput, + QueryCommand, + QueryCommandInput, + paginateQuery, + DynamoDBDocumentPaginationConfiguration, +} from '@aws-sdk/lib-dynamodb'; +import { CloudFormationClient, DescribeStacksCommand } from '@aws-sdk/client-cloudformation'; +import { throttlingBackOff } from '@aws-accelerator/utils'; + +const marshallOptions = { + convertEmptyValues: false, + //overriding default value of false + removeUndefinedValues: true, + convertClassInstanceToMap: false, +}; +const unmarshallOptions = { + wrapNumbers: false, +}; +const translateConfig = { marshallOptions, unmarshallOptions }; +const dynamodbClient = new DynamoDBClient({}); +const documentClient = DynamoDBDocumentClient.from(dynamodbClient, translateConfig); +const serviceCatalogClient = new AWS.ServiceCatalog(); +const cloudformationClient = new CloudFormationClient({}); +let organizationsClient: AWS.Organizations; +const paginationConfig: DynamoDBDocumentPaginationConfiguration = { + client: documentClient, + pageSize: 100, +}; + +type AccountToAdd = { + name: string; + description: string; + email: string; + enableGovCloud?: boolean; + organizationalUnitId: string; +}; + +type OrganizationalUnitKeys = { + acceleratorKey: string; + awsKey: string; +}; + +type DDBItem = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any; +}; +type DDBItems = Array; + +/** +* validate-environment - lambda handler +* +* @param event +* @returns +*/ +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + Status: string; + } + | undefined +> { + const partition = event.ResourceProperties['partition']; + const configTableName = event.ResourceProperties['configTableName']; + const newOrgAccountsTableName = event.ResourceProperties['newOrgAccountsTableName']; + const newCTAccountsTableName = event.ResourceProperties['newCTAccountsTableName']; + const controlTowerEnabled = event.ResourceProperties['controlTowerEnabled']; + const commitId = event.ResourceProperties['commitId']; + const stackName = event.ResourceProperties['stackName']; + const validationErrors: string[] = []; + const ctAccountsToAdd = []; + const orgAccountsToAdd = []; + + if (partition === 'aws-us-gov') { + organizationsClient = new AWS.Organizations({ region: 'us-gov-west-1' }); + } else { + organizationsClient = new AWS.Organizations({ region: 'us-east-1' }); + } + + console.log(stackName); + switch (event.RequestType) { + case 'Create': + case 'Update': + // if stack rollback is in progress don't do anything + // the stack may have failed as the results of errors + // from this construct + // when rolling back this construct will execute and + // fail again preventing stack rollback + if (await isStackInRollback(stackName)) { + return { + Status: 'SUCCESS', + }; + } + console.log(`Configuration repository commit id ${commitId}`); + // get accounts from organizations + const organizationAccounts = await getOrganizationAccounts(); + + const mandatoryAccounts = await getConfigFromTableForCommit(configTableName, 'mandatoryAccount', commitId); + const workloadAccounts = await getConfigFromTableForCommit(configTableName, 'workloadAccount', commitId); + if (controlTowerEnabled === 'true' && mandatoryAccounts) { + // confirm mandatory accounts exist in aws + for (const mandatoryAccount of mandatoryAccounts) { + const existingAccount = organizationAccounts.find(item => item.Email == mandatoryAccount['acceleratorKey']); + if (existingAccount?.Status == 'ACTIVE') { + console.log(`Mandatory Account ${mandatoryAccount['acceleratorKey']} exists.`); + } else { + validationErrors.push( + `Mandatory account ${mandatoryAccount['acceleratorKey']} does not exist in AWS or is suspended`, + ); + } + } + + const validateOrganizationalUnits = await validateOrganizationalUnitsExist(configTableName, commitId); + validationErrors.push(...validateOrganizationalUnits); + + const validateAccountsAreInOu = await validateAccountsInOu(configTableName, commitId); + validationErrors.push(...validateAccountsAreInOu); + + // retrieve all of the accounts provisioned in control tower + const provisionedControlTowerAccounts = await getControlTowerProvisionedAccounts(); + // confirm workload accounts exist in control tower without errors + if (workloadAccounts) { + for (const workloadAccount of workloadAccounts) { + const accountConfig = JSON.parse(workloadAccount['dataBag']); + const accountName = accountConfig['name']; + const provisionedControlTowerAccount = provisionedControlTowerAccounts.find( + pcta => pcta.Name == accountName, + ); + if (provisionedControlTowerAccount) { + switch (provisionedControlTowerAccount['Status']) { + case 'AVAILABLE': + break; + case 'TAINTED': + validationErrors.push( + `AWS Account ${workloadAccount['acceleratorKey']} is TAINTED state. Message: ${provisionedControlTowerAccount.StatusMessage}. Check Service Catalog`, + ); + break; + case 'ERROR': + validationErrors.push( + `AWS Account ${workloadAccount['acceleratorKey']} is in ERROR state. Message: ${provisionedControlTowerAccount.StatusMessage}. Check Service Catalog`, + ); + break; + case 'UNDER_CHANGE': + break; + case 'PLAN_IN_PROGRESS': + break; + } + } else { + // confirm account doesn't exist in control tower with a different name + // if enrolled directly in console the name in service catalog won't match + // look up by physical id if it exists + const checkAccountId = organizationAccounts.find(oa => oa.Email == workloadAccount['acceleratorKey']); + if (checkAccountId) { + const provisionedControlTowerAccount = provisionedControlTowerAccounts.find( + pcta => pcta.PhysicalId === checkAccountId.Id, + ); + if ( + provisionedControlTowerAccount?.Status === 'TAINTED' || + provisionedControlTowerAccount?.Status === 'ERROR' + ) { + validationErrors.push( + `AWS Account ${workloadAccount['acceleratorKey']} is in ERROR state. Message: ${provisionedControlTowerAccount.StatusMessage}. Check Service Catalog`, + ); + } + if (!provisionedControlTowerAccount) { + ctAccountsToAdd.push(workloadAccount); + } + } else { + ctAccountsToAdd.push(workloadAccount); + } + } + } + } + } + + // find organization accounts that need to be created + console.log(`controlTowerEnabled value: ${controlTowerEnabled}`); + if (controlTowerEnabled === 'false' && mandatoryAccounts) { + for (const mandatoryAccount of mandatoryAccounts) { + const mandatoryOrganizationAccount = organizationAccounts.find( + item => item.Email == mandatoryAccount['acceleratorKey'], + ); + if (mandatoryOrganizationAccount) { + if (mandatoryOrganizationAccount.Status !== 'ACTIVE') { + validationErrors.push( + `Mandatory account ${mandatoryAccount['acceleratorKey']} is in ${mandatoryOrganizationAccount.Status}`, + ); + } + } else { + orgAccountsToAdd.push(mandatoryAccount); + } + } + } + if (workloadAccounts) { + for (const workloadAccount of workloadAccounts) { + const organizationAccount = organizationAccounts.find( + item => item.Email == workloadAccount['acceleratorKey'], + ); + if (organizationAccount) { + if (organizationAccount.Status !== 'ACTIVE') { + validationErrors.push( + `Workload account ${workloadAccount['acceleratorKey']} is in ${organizationAccount.Status}`, + ); + } + } else { + const accountConfig = JSON.parse(workloadAccount['dataBag']); + if (controlTowerEnabled === 'false' || accountConfig['enableGovCloud']) { + orgAccountsToAdd.push(workloadAccount); + } + } + } + } + + const organizationalUnitKeys = await getOUKeys(configTableName, commitId); + // put accounts to create in DynamoDb + console.log(`Org Accounts to add: ${JSON.stringify(orgAccountsToAdd)}`); + for (const account of orgAccountsToAdd) { + const accountOu = organizationalUnitKeys.find(item => item.acceleratorKey === account['ouName']); + const parsedDataBag = JSON.parse(account['dataBag']); + let accountConfig: AccountToAdd; + if (accountOu?.awsKey) { + accountConfig = { + name: parsedDataBag['name'], + email: account['acceleratorKey'], + description: parsedDataBag['description'], + enableGovCloud: parsedDataBag['enableGovCloud'] || false, + organizationalUnitId: accountOu?.awsKey, + }; + const params: PutCommandInput = { + TableName: newOrgAccountsTableName, + Item: { + accountEmail: accountConfig.email, + accountConfig: JSON.stringify(accountConfig), + }, + }; + await throttlingBackOff(() => documentClient.send(new PutCommand(params))); + } else { + // should not get here we just created and validated all of the ou's. + validationErrors.push(`Unable to find Organizational Unit ${account['ouName']} in configuration`); + } + } + + console.log(`CT Accounts to add: ${JSON.stringify(ctAccountsToAdd)}`); + for (const account of ctAccountsToAdd) { + const accountOu = organizationalUnitKeys.find(item => item.acceleratorKey === account['ouName']); + const parsedDataBag = JSON.parse(account['dataBag']); + let accountConfig: AccountToAdd; + if (accountOu?.awsKey) { + accountConfig = { + name: parsedDataBag['name'], + email: account['acceleratorKey'], + description: parsedDataBag['description'], + organizationalUnitId: await getOuName(account['ouName']), + }; + const params: PutCommandInput = { + TableName: newCTAccountsTableName, + Item: { + accountEmail: accountConfig.email, + accountConfig: JSON.stringify(accountConfig), + }, + }; + await throttlingBackOff(() => documentClient.send(new PutCommand(params))); + } else { + // should not get here we just created and validated all of the ou's. + validationErrors.push(`Unable to find Organizational Unit ${account['ouName']} in configuration`); + } + } + + console.log(`validationErrors: ${JSON.stringify(validationErrors)}`); + + if (validationErrors.length > 0) { + throw new Error(validationErrors.toString()); + } + + return { + Status: 'SUCCESS', + }; + + case 'Delete': + // Do Nothing + return { + Status: 'SUCCESS', + }; + } +} + +async function getOuName(name: string): Promise { + const result = name.split('/').pop(); + if (result === undefined) { + return name; + } + return result; +} + +async function getControlTowerProvisionedAccounts(): Promise { + const provisionedProducts: AWS.ServiceCatalog.ProvisionedProductAttribute[] = []; + let nextToken: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => + serviceCatalogClient + .searchProvisionedProducts({ + Filters: { + SearchQuery: ['type: CONTROL_TOWER_ACCOUNT'], + }, + AccessLevelFilter: { + Key: 'Account', + Value: 'self', + }, + PageToken: nextToken, + }) + .promise(), + ); + + for (const product of page.ProvisionedProducts ?? []) { + provisionedProducts.push(product); + } + nextToken = page.NextPageToken; + } while (nextToken); + + //console.log(`Provisioned Control Tower Accounts ${JSON.stringify(provisionedProducts)}`); + return provisionedProducts; +} + +async function getOrganizationAccounts(): Promise { + const organizationAccounts: AWS.Organizations.Account[] = []; + let nextToken: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => organizationsClient.listAccounts({ NextToken: nextToken }).promise()); + for (const account of page.Accounts ?? []) { + organizationAccounts.push(account); + } + nextToken = page.NextToken; + } while (nextToken); + + return organizationAccounts; +} + +async function getConfigFromTableForCommit( + configTableName: string, + dataType: string, + commitId: string, +): Promise { + const params: QueryCommandInput = { + TableName: configTableName, + KeyConditionExpression: 'dataType = :hkey', + ExpressionAttributeValues: { + ':hkey': dataType, + ':commitId': commitId, + }, + FilterExpression: 'contains (commitId, :commitId)', + }; + const items: DDBItems = []; + const paginator = paginateQuery(paginationConfig, params); + for await (const page of paginator) { + if (page.Items) { + for (const item of page.Items) { + items.push(item); + } + } + } + return items; +} + +async function validateOrganizationalUnitsExist(configTableName: string, commitId: string): Promise { + const errors: string[] = []; + const organizationalUnitKeys = await getOUKeys(configTableName, commitId); + const missingOrganizationalUnits = organizationalUnitKeys.filter(item => item.awsKey === undefined); + + if (missingOrganizationalUnits.length > 0) { + for (const item of missingOrganizationalUnits) { + console.log(`Organizational Unit ${item.acceleratorKey} does not exist in AWS`); + errors.push( + `Organizational Unit ${item.acceleratorKey} does not exist in AWS. Either remove from configuration or add OU via console`, + ); + } + } + return errors; +} + +async function validateAccountsInOu(configTableName: string, commitId: string): Promise { + const errors: string[] = []; + let nextToken: string | undefined = undefined; + const organizationalUnitKeys = await getOUKeys(configTableName, commitId); + + const workloadAccountParams: QueryCommandInput = { + TableName: configTableName, + KeyConditionExpression: 'dataType = :hkey', + ExpressionAttributeValues: { + ':hkey': 'workloadAccount', + }, + ProjectionExpression: 'acceleratorKey, awsKey, ouName', + }; + const workloadAccountResponse = await throttlingBackOff(() => + documentClient.send(new QueryCommand(workloadAccountParams)), + ); + const workloadAccountKeys: { acceleratorKey: string; awsKey: string; ouName: string }[] = []; + if (workloadAccountResponse.Items) { + for (const item of workloadAccountResponse.Items) { + workloadAccountKeys.push({ + acceleratorKey: item['acceleratorKey'], + awsKey: item['awsKey'], + ouName: item['ouName'], + }); + } + } + //console.log(workloadAccountKeys); + + const mandatoryAccountParams: QueryCommandInput = { + TableName: configTableName, + KeyConditionExpression: 'dataType = :hkey', + ExpressionAttributeValues: { + ':hkey': 'mandatoryAccount', + }, + ProjectionExpression: 'acceleratorKey, awsKey, ouName', + }; + const mandatoryAccountResponse = await throttlingBackOff(() => + documentClient.send(new QueryCommand(mandatoryAccountParams)), + ); + const mandatoryAccountKeys: { acceleratorKey: string; awsKey: string; ouName: string }[] = []; + if (mandatoryAccountResponse.Items) { + for (const item of mandatoryAccountResponse.Items) { + mandatoryAccountKeys.push({ + acceleratorKey: item['acceleratorKey'], + awsKey: item['awsKey'], + ouName: item['ouName'], + }); + } + } + //console.log(mandatoryAccountKeys); + + const accountKeys = mandatoryAccountKeys; + accountKeys.push(...workloadAccountKeys); + + for (const ou of organizationalUnitKeys) { + const children: string[] = []; + // if we don't have an awsKey then we didn't find the OU don't attempt to lookup child accounts + if (!ou.awsKey) { + continue; + } + do { + const page = await throttlingBackOff(() => + organizationsClient.listChildren({ ChildType: 'ACCOUNT', ParentId: ou.awsKey, NextToken: nextToken }).promise(), + ); + for (const child of page.Children ?? []) { + const account = accountKeys.find(item => item.awsKey === child.Id); + if (account) { + children.push(account.awsKey); + if (account.ouName !== ou.acceleratorKey) { + errors.push( + `Account ${account.acceleratorKey} with account id ${account.awsKey} is not in the correct OU. Account is in the ou named ${ou.acceleratorKey} and should be in ${account.ouName}`, + ); + } + } else { + errors.push(`Found account with id ${child.Id} in OU ${ou.acceleratorKey} that is not in the configuration.`); + } + } + nextToken = page.NextToken; + } while (nextToken); + console.log(`OU Name: ${ou.acceleratorKey} Child Account ID's: ${children}`); + } + + return errors; +} + +async function getOUKeys(configTableName: string, commitId: string): Promise { + const organizationParams: QueryCommandInput = { + TableName: configTableName, + KeyConditionExpression: 'dataType = :hkey', + ExpressionAttributeValues: { + ':hkey': 'organization', + ':commitId': commitId, + }, + FilterExpression: 'contains (commitId, :commitId)', + ProjectionExpression: 'acceleratorKey, awsKey', + }; + const organizationResponse = await throttlingBackOff(() => documentClient.send(new QueryCommand(organizationParams))); + const ouKeys: OrganizationalUnitKeys[] = []; + if (organizationResponse.Items) { + for (const item of organizationResponse.Items) { + ouKeys.push({ acceleratorKey: item['acceleratorKey'], awsKey: item['awsKey'] }); + } + } + //console.log(ouKeys); + return ouKeys; +} + +async function isStackInRollback(stackName: string): Promise { + const response = await throttlingBackOff(() => + cloudformationClient.send(new DescribeStacksCommand({ StackName: stackName })), + ); + if (response.Stacks && response.Stacks[0].StackStatus == 'UPDATE_ROLLBACK_IN_PROGRESS') { + return true; + } + return false; +} diff --git a/source/packages/@aws-accelerator/accelerator/lib/lambdas/validate-environment/package.json b/source/packages/@aws-accelerator/accelerator/lib/lambdas/validate-environment/package.json new file mode 100644 index 000000000..ccdfbe55b --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/lib/lambdas/validate-environment/package.json @@ -0,0 +1,50 @@ +{ + "name": "@aws-accelerator/lambdas-validate-environment-config", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --sourcemap --platform=node --target=node14 --external:aws-sdk --outfile=./dist/index.js index.ts", + "test": "", + "testreport": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "@aws-sdk/types": "3.78.0", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2", + "ts-node": "10.4.0" + }, + "peerDependencies": { + "eslint": "8.4.1" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1001.0", + "@aws-sdk/lib-dynamodb": "3.87.0", + "@aws-sdk/client-dynamodb": "3.87.0", + "@aws-sdk/client-cloudformation": "3.87.0" + } +} diff --git a/source/packages/@aws-accelerator/accelerator/lib/lambdas/validate-environment/tsconfig.json b/source/packages/@aws-accelerator/accelerator/lib/lambdas/validate-environment/tsconfig.json new file mode 100644 index 000000000..ebcb98a79 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/lib/lambdas/validate-environment/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} diff --git a/source/packages/@aws-accelerator/accelerator/lib/load-config-table.ts b/source/packages/@aws-accelerator/accelerator/lib/load-config-table.ts new file mode 100644 index 000000000..093c01f3c --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/lib/load-config-table.ts @@ -0,0 +1,147 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { v4 as uuidv4 } from 'uuid'; +import { Construct } from 'constructs'; +import { Duration } from 'aws-cdk-lib'; +import path = require('path'); + +export interface LoadAcceleratorConfigTableProps { + readonly acceleratorConfigTable: cdk.aws_dynamodb.ITable; + readonly configRepositoryName: string; + readonly managementAccountEmail: string; + readonly logArchiveAccountEmail: string; + readonly auditAccountEmail: string; + readonly configS3Bucket: string; + readonly organizationsConfigS3Key: string; + readonly accountConfigS3Key: string; + readonly commitId: string; + readonly partition: string; + readonly managementAccountId: string; + readonly region: string; + readonly stackName: string; + /** + * Custom resource lambda log group encryption key + */ + readonly kmsKey: cdk.aws_kms.Key; + /** + * Custom resource lambda log retention in days + */ + readonly logRetentionInDays: number; +} + +/** + * Class Load Accelerator Config Table + */ +export class LoadAcceleratorConfigTable extends Construct { + public readonly id: string; + + constructor(scope: Construct, id: string, props: LoadAcceleratorConfigTableProps) { + super(scope, id); + + const LOAD_CONFIG_TABLE_RESOURCE_TYPE = 'Custom::LoadAcceleratorConfigTable'; + + // + // Function definition for the custom resource + // + const provider = cdk.CustomResourceProvider.getOrCreateProvider(this, LOAD_CONFIG_TABLE_RESOURCE_TYPE, { + codeDirectory: path.join(__dirname, 'lambdas/load-config-table/dist'), + runtime: cdk.CustomResourceProviderRuntime.NODEJS_14_X, + timeout: Duration.minutes(15), + policyStatements: [ + { + Sid: 'organizations', + Effect: 'Allow', + Action: [ + 'organizations:ListAccounts', + 'organizations:ListRoots', + 'organizations:ListOrganizationalUnitsForParent', + ], + Resource: '*', + }, + { + Sid: 'configTable', + Effect: 'Allow', + Action: ['dynamodb:UpdateItem', 'dynamodb:PutItem'], + Resource: [props.acceleratorConfigTable.tableArn], + }, + { + Sid: 'kms', + Effect: 'Allow', + Action: ['kms:Encrypt', 'kms:Decrypt', 'kms:GenerateDataKey*', 'kms:DescribeKey'], + Resource: [props.acceleratorConfigTable.encryptionKey?.keyArn], + }, + { + Sid: 's3', + Effect: 'Allow', + Action: ['s3:GetObject'], + Resource: [ + `arn:${cdk.Stack.of(this).partition}:s3:::cdk-accel-assets-${cdk.Stack.of(this).account}-${ + cdk.Stack.of(this).region + }/*`, + ], + }, + { + Sid: 'cloudFormation', + Effect: 'Allow', + Action: ['cloudformation:DescribeStacks'], + Resource: [ + `arn:${props.partition}:cloudformation:${props.region}:${props.managementAccountId}:stack/${props.stackName}*`, + ], + }, + ], + }); + + // + // Custom Resource definition. We want this resource to be evaluated on + // every CloudFormation update, so we generate a new uuid to force + // re-evaluation. + // + const resource = new cdk.CustomResource(this, 'Resource', { + resourceType: LOAD_CONFIG_TABLE_RESOURCE_TYPE, + serviceToken: provider.serviceToken, + properties: { + configTableName: props.acceleratorConfigTable.tableName, + configRepositoryName: props.configRepositoryName, + managementAccountEmail: props.managementAccountEmail, + auditAccountEmail: props.auditAccountEmail, + logArchiveAccountEmail: props.logArchiveAccountEmail, + configS3Bucket: props.configS3Bucket, + organizationsConfigS3Key: props.organizationsConfigS3Key, + accountConfigS3Key: props.accountConfigS3Key, + commitId: props.commitId, + partition: props.partition, + stackName: props.stackName, + uuid: uuidv4(), // Generates a new UUID to force the resource to update + }, + }); + + /** + * Singleton pattern to define the log group for the singleton function + * in the stack + */ + const stack = cdk.Stack.of(scope); + const logGroup = + (stack.node.tryFindChild(`${provider.node.id}LogGroup`) as cdk.aws_logs.LogGroup) ?? + new cdk.aws_logs.LogGroup(stack, `${provider.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${(provider.node.findChild('Handler') as cdk.aws_lambda.CfnFunction).ref}`, + retention: props.logRetentionInDays, + encryptionKey: props.kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + resource.node.addDependency(logGroup); + + this.id = resource.ref; + } +} diff --git a/source/packages/@aws-accelerator/accelerator/lib/logger.ts b/source/packages/@aws-accelerator/accelerator/lib/logger.ts new file mode 100644 index 000000000..881f60356 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/lib/logger.ts @@ -0,0 +1,41 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as winston from 'winston'; + +const printf = winston.format.printf(info => `[${info['timestamp']}] - ${info.level}: ${info.message}`); +const timeFormat = 'YYYY-MM-DD HH:mm:ss'; + +export const Logger = winston.createLogger({ + level: process.env['LOG_LEVEL'] ?? 'debug', + format: winston.format.combine(winston.format.timestamp({ format: timeFormat }), winston.format.align(), printf), + transports: [ + new winston.transports.File({ + filename: 'error.log', + level: 'error', + }), + new winston.transports.File({ + filename: 'combined.log', + }), + new winston.transports.Console({ + format: winston.format.combine( + winston.format.colorize(), + winston.format.timestamp({ format: timeFormat }), + winston.format.align(), + printf, + ), + }), + ], +}); + +winston.add(Logger); diff --git a/source/packages/@aws-accelerator/accelerator/lib/pipeline.ts b/source/packages/@aws-accelerator/accelerator/lib/pipeline.ts new file mode 100644 index 000000000..957d71fab --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/lib/pipeline.ts @@ -0,0 +1,536 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import * as codebuild from 'aws-cdk-lib/aws-codebuild'; +import * as codecommit from 'aws-cdk-lib/aws-codecommit'; +import * as codepipeline from 'aws-cdk-lib/aws-codepipeline'; +import * as codepipeline_actions from 'aws-cdk-lib/aws-codepipeline-actions'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import { Construct } from 'constructs'; + +import { Bucket, BucketEncryptionType } from '@aws-accelerator/constructs'; + +import { AcceleratorStage } from './accelerator-stage'; +import * as config_repository from './config-repository'; +import { AcceleratorToolkitCommand } from './toolkit'; + +/** + * + */ +export interface AcceleratorPipelineProps { + readonly toolkitRole: cdk.aws_iam.Role; + readonly sourceRepository: string; + readonly sourceRepositoryOwner: string; + readonly sourceRepositoryName: string; + readonly sourceBranchName: string; + readonly enableApprovalStage: boolean; + readonly qualifier?: string; + readonly managementAccountId?: string; + readonly managementAccountRoleName?: string; + readonly managementAccountEmail: string; + readonly logArchiveAccountEmail: string; + readonly auditAccountEmail: string; + /** + * List of email addresses to be notified when pipeline is waiting for manual approval stage. + * If pipeline do not have approval stage enabled, this value will have no impact. + */ + readonly approvalStageNotifyEmailList?: string; + readonly partition: string; +} + +/** + * AWS Accelerator Pipeline Class, which creates the pipeline for AWS Landing zone + */ +export class AcceleratorPipeline extends Construct { + private readonly pipelineRole: iam.Role; + private readonly toolkitProject: codebuild.PipelineProject; + private readonly buildOutput: codepipeline.Artifact; + private readonly acceleratorRepoArtifact: codepipeline.Artifact; + private readonly configRepoArtifact: codepipeline.Artifact; + + constructor(scope: Construct, id: string, props: AcceleratorPipelineProps) { + super(scope, id); + + let pipelineAccountEnvVariables: { [p: string]: codebuild.BuildEnvironmentVariable } | undefined; + + if (props.managementAccountId && props.managementAccountRoleName) { + pipelineAccountEnvVariables = { + MANAGEMENT_ACCOUNT_ID: { + type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: props.managementAccountId, + }, + MANAGEMENT_ACCOUNT_ROLE_NAME: { + type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: props.managementAccountRoleName, + }, + }; + } + + // Get installer key + const installerKey = cdk.aws_kms.Key.fromKeyArn( + this, + 'AcceleratorKey', + cdk.aws_ssm.StringParameter.valueForStringParameter( + this, + props.qualifier + ? `/accelerator/${props.qualifier}/installer/kms/key-arn` + : '/accelerator/installer/kms/key-arn', + ), + ) as cdk.aws_kms.Key; + + const bucket = new Bucket(this, 'SecureBucket', { + encryptionType: BucketEncryptionType.SSE_KMS, + s3BucketName: `${props.qualifier ?? 'aws-accelerator'}-pipeline-${cdk.Stack.of(this).account}-${ + cdk.Stack.of(this).region + }`, + kmsKey: installerKey, + serverAccessLogsBucketName: cdk.aws_ssm.StringParameter.valueForStringParameter( + this, + props.qualifier + ? `/accelerator/${props.qualifier}/installer-access-logs-bucket-name` + : '/accelerator/installer-access-logs-bucket-name', + ), + }); + + const configRepository = new config_repository.ConfigRepository(this, 'ConfigRepository', { + repositoryName: `${props.qualifier ?? 'aws-accelerator'}-config`, + repositoryBranchName: 'main', + description: + 'AWS Accelerator configuration repository, created and initialized with default config file by pipeline', + managementAccountEmail: props.managementAccountEmail, + logArchiveAccountEmail: props.logArchiveAccountEmail, + auditAccountEmail: props.auditAccountEmail, + }); + + /** + * Pipeline + */ + this.pipelineRole = new iam.Role(this, 'PipelineRole', { + assumedBy: new iam.ServicePrincipal('codepipeline.amazonaws.com'), + }); + + const pipeline = new codepipeline.Pipeline(this, 'Resource', { + pipelineName: props.qualifier ? `${props.qualifier}-pipeline` : 'AWSAccelerator-Pipeline', + artifactBucket: bucket.getS3Bucket(), + role: this.pipelineRole, + }); + + this.acceleratorRepoArtifact = new codepipeline.Artifact('Source'); + this.configRepoArtifact = new codepipeline.Artifact('Config'); + + let sourceAction: + | cdk.aws_codepipeline_actions.CodeCommitSourceAction + | cdk.aws_codepipeline_actions.GitHubSourceAction; + + if (props.sourceRepository === 'codecommit') { + sourceAction = new codepipeline_actions.CodeCommitSourceAction({ + actionName: 'Source', + repository: codecommit.Repository.fromRepositoryName(this, 'SourceRepo', props.sourceRepositoryName), + branch: props.sourceBranchName, + output: this.acceleratorRepoArtifact, + trigger: codepipeline_actions.CodeCommitTrigger.NONE, + }); + } else { + sourceAction = new cdk.aws_codepipeline_actions.GitHubSourceAction({ + actionName: 'Source', + owner: props.sourceRepositoryOwner, + repo: props.sourceRepositoryName, + branch: props.sourceBranchName, + oauthToken: cdk.SecretValue.secretsManager('accelerator/github-token'), + output: this.acceleratorRepoArtifact, + trigger: cdk.aws_codepipeline_actions.GitHubTrigger.NONE, + }); + } + + pipeline.addStage({ + stageName: 'Source', + actions: [ + sourceAction, + new codepipeline_actions.CodeCommitSourceAction({ + actionName: 'Configuration', + repository: configRepository.getRepository(), + branch: 'main', + output: this.configRepoArtifact, + trigger: codepipeline_actions.CodeCommitTrigger.NONE, + variablesNamespace: 'Config-Vars', + }), + ], + }); + + /** + * Build Stage + */ + const buildRole = new iam.Role(this, 'BuildRole', { + assumedBy: new iam.ServicePrincipal('codebuild.amazonaws.com'), + }); + + const buildProject = new codebuild.PipelineProject(this, 'BuildProject', { + projectName: props.qualifier ? `${props.qualifier}-build-project` : 'AWSAccelerator-BuildProject', + encryptionKey: installerKey, + role: buildRole, + buildSpec: codebuild.BuildSpec.fromObject({ + version: '0.2', + phases: { + install: { + 'runtime-versions': { + nodejs: 14, + }, + }, + build: { + commands: ['env', 'cd source', 'yarn install', 'yarn lerna link', 'yarn build'], + }, + }, + artifacts: { + files: ['**/*'], + 'enable-symlinks': 'yes', + }, + }), + environment: { + buildImage: codebuild.LinuxBuildImage.STANDARD_5_0, + privileged: true, // Allow access to the Docker daemon + computeType: codebuild.ComputeType.MEDIUM, + environmentVariables: { + NODE_OPTIONS: { + type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: '--max_old_space_size=4096', + }, + }, + }, + cache: codebuild.Cache.local(codebuild.LocalCacheMode.SOURCE), + }); + + this.buildOutput = new codepipeline.Artifact('Build'); + + pipeline.addStage({ + stageName: 'Build', + actions: [ + new codepipeline_actions.CodeBuildAction({ + actionName: 'Build', + project: buildProject, + input: this.acceleratorRepoArtifact, + extraInputs: [this.configRepoArtifact], + outputs: [this.buildOutput], + role: this.pipelineRole, + }), + ], + }); + + /** + * Deploy Stage + */ + + this.toolkitProject = new codebuild.PipelineProject(this, 'ToolkitProject', { + projectName: props.qualifier ? `${props.qualifier}-toolkit-project` : 'AWSAccelerator-ToolkitProject', + encryptionKey: installerKey, + role: props.toolkitRole, + timeout: cdk.Duration.hours(5), + buildSpec: codebuild.BuildSpec.fromObject({ + version: '0.2', + phases: { + install: { + 'runtime-versions': { + nodejs: 14, + }, + }, + build: { + commands: [ + 'env', + 'cd source', + 'cd packages/@aws-accelerator/accelerator', + `if [ -z "\${ACCELERATOR_STAGE}" ]; then yarn run ts-node --transpile-only cdk.ts synth --require-approval never --config-dir $CODEBUILD_SRC_DIR_Config --partition ${cdk.Aws.PARTITION}; fi`, + `if [ ! -z "\${ACCELERATOR_STAGE}" ]; then yarn run ts-node --transpile-only cdk.ts synth --stage $ACCELERATOR_STAGE --require-approval never --config-dir $CODEBUILD_SRC_DIR_Config --partition ${cdk.Aws.PARTITION}; fi`, + `yarn run ts-node --transpile-only cdk.ts --require-approval never $CDK_OPTIONS --config-dir $CODEBUILD_SRC_DIR_Config --partition ${cdk.Aws.PARTITION} --app cdk.out`, + ], + }, + }, + }), + environment: { + buildImage: codebuild.LinuxBuildImage.STANDARD_5_0, + privileged: true, // Allow access to the Docker daemon + computeType: codebuild.ComputeType.MEDIUM, + environmentVariables: { + NODE_OPTIONS: { + type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: '--max_old_space_size=4096', + }, + CDK_NEW_BOOTSTRAP: { + type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: '1', + }, + ACCELERATOR_QUALIFIER: { + type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: props.qualifier ? props.qualifier : 'aws-accelerator', + }, + ...pipelineAccountEnvVariables, + }, + }, + cache: codebuild.Cache.local(codebuild.LocalCacheMode.SOURCE), + }); + + // /** + // * The Prepare stage is used to verify that all prerequisites have been made and that the + // * Accelerator can be deployed into the environment + // * Creates the accounts + // * Creates the ou's if control tower is not enabled + // */ + pipeline.addStage({ + stageName: 'Prepare', + actions: [this.createToolkitStage({ actionName: 'Prepare', command: 'deploy', stage: AcceleratorStage.PREPARE })], + }); + + pipeline.addStage({ + stageName: 'Accounts', + actions: [ + this.createToolkitStage({ actionName: 'Accounts', command: 'deploy', stage: AcceleratorStage.ACCOUNTS }), + ], + }); + + pipeline.addStage({ + stageName: 'Bootstrap', + actions: [this.createToolkitStage({ actionName: 'Bootstrap', command: `bootstrap` })], + }); + + if (props.enableApprovalStage) { + let notificationTopic: cdk.aws_sns.Topic | undefined; + + if (props.partition === 'aws') { + notificationTopic = new cdk.aws_sns.Topic(this, 'ManualApprovalActionTopic', { + topicName: (props.qualifier ? props.qualifier : 'aws-accelerator') + '-pipeline-review-topic', + displayName: (props.qualifier ? props.qualifier : 'aws-accelerator') + '-pipeline-review-topic', + masterKey: installerKey, + }); + } + + pipeline.addStage({ + stageName: 'Review', + actions: [ + this.createToolkitStage({ actionName: 'Diff', command: 'diff', runOrder: 1 }), + new codepipeline_actions.ManualApprovalAction({ + actionName: 'Approve', + runOrder: 2, + additionalInformation: 'See previous stage (Diff) for changes.', + notificationTopic, + notifyEmails: notificationTopic + ? props.approvalStageNotifyEmailList + ? props.approvalStageNotifyEmailList.split(',') + : undefined + : undefined, + }), + ], + }); + } + + /** + * The Logging stack establishes all the logging assets that are needed in + * all the accounts and will configure: + * + * - An S3 Access Logs bucket for every region in every account + * - The Central Logs bucket in the log-archive account + * + */ + pipeline.addStage({ + stageName: 'Logging', + actions: [ + this.createToolkitStage({ actionName: 'Key', command: 'deploy', stage: AcceleratorStage.KEY, runOrder: 1 }), + this.createToolkitStage({ + actionName: 'Logging', + command: 'deploy', + stage: AcceleratorStage.LOGGING, + runOrder: 2, + }), + ], + }); + + pipeline.addStage({ + stageName: 'Organization', + actions: [ + this.createToolkitStage({ + actionName: 'Organizations', + command: 'deploy', + stage: AcceleratorStage.ORGANIZATIONS, + }), + ], + }); + + pipeline.addStage({ + stageName: 'SecurityAudit', + actions: [ + this.createToolkitStage({ + actionName: 'SecurityAudit', + command: 'deploy', + stage: AcceleratorStage.SECURITY_AUDIT, + }), + ], + }); + + // pipeline.addStage({ + // stageName: 'Dependencies', + // actions: [this.createToolkitStage('Dependencies', `deploy --stage ${AcceleratorStage.DEPENDENCIES}`)], + // }); + + pipeline.addStage({ + stageName: 'Deploy', + actions: [ + this.createToolkitStage({ + actionName: 'Network_Prepare', + command: 'deploy', + stage: AcceleratorStage.NETWORK_PREP, + runOrder: 1, + }), + this.createToolkitStage({ + actionName: 'Security', + command: 'deploy', + stage: AcceleratorStage.SECURITY, + runOrder: 1, + }), + this.createToolkitStage({ + actionName: 'Operations', + command: 'deploy', + stage: AcceleratorStage.OPERATIONS, + runOrder: 1, + }), + this.createToolkitStage({ + actionName: 'Network_VPCs', + command: 'deploy', + stage: AcceleratorStage.NETWORK_VPC, + runOrder: 2, + }), + this.createToolkitStage({ + actionName: 'Security_Resources', + command: 'deploy', + stage: AcceleratorStage.SECURITY_RESOURCES, + runOrder: 2, + }), + this.createToolkitStage({ + actionName: 'Network_Associations', + command: 'deploy', + stage: AcceleratorStage.NETWORK_ASSOCIATIONS, + runOrder: 3, + }), + this.createToolkitStage({ + actionName: 'Finalize', + command: 'deploy', + stage: AcceleratorStage.FINALIZE, + runOrder: 4, + }), + ], + }); + + // Enable Pipeline notification + if (props.partition === 'aws') { + const codeStarNotificationsRole = new cdk.aws_iam.CfnServiceLinkedRole( + this, + 'AWSServiceRoleForCodeStarNotifications', + { + awsServiceName: 'codestar-notifications.amazonaws.com', + description: 'Allows AWS CodeStar Notifications to access Amazon CloudWatch Events on your behalf', + }, + ); + pipeline.node.addDependency(codeStarNotificationsRole); + + const acceleratorStatusTopic = new cdk.aws_sns.Topic(this, 'AcceleratorStatusTopic', { + topicName: (props.qualifier ? props.qualifier : 'aws-accelerator') + '-pipeline-status-topic', + displayName: (props.qualifier ? props.qualifier : 'aws-accelerator') + '-pipeline-status-topic', + masterKey: installerKey, + }); + + acceleratorStatusTopic.grantPublish(pipeline.role); + + pipeline.notifyOn('AcceleratorPipelineStatusNotification', acceleratorStatusTopic, { + events: [ + cdk.aws_codepipeline.PipelineNotificationEvents.MANUAL_APPROVAL_FAILED, + cdk.aws_codepipeline.PipelineNotificationEvents.MANUAL_APPROVAL_NEEDED, + cdk.aws_codepipeline.PipelineNotificationEvents.MANUAL_APPROVAL_SUCCEEDED, + cdk.aws_codepipeline.PipelineNotificationEvents.PIPELINE_EXECUTION_CANCELED, + cdk.aws_codepipeline.PipelineNotificationEvents.PIPELINE_EXECUTION_FAILED, + cdk.aws_codepipeline.PipelineNotificationEvents.PIPELINE_EXECUTION_RESUMED, + cdk.aws_codepipeline.PipelineNotificationEvents.PIPELINE_EXECUTION_STARTED, + cdk.aws_codepipeline.PipelineNotificationEvents.PIPELINE_EXECUTION_SUCCEEDED, + cdk.aws_codepipeline.PipelineNotificationEvents.PIPELINE_EXECUTION_SUPERSEDED, + ], + }); + + // Pipeline failure status topic and alarm + const acceleratorFailedStatusTopic = new cdk.aws_sns.Topic(this, 'AcceleratorFailedStatusTopic', { + topicName: (props.qualifier ? props.qualifier : 'aws-accelerator') + '-pipeline-failed-status-topic', + displayName: (props.qualifier ? props.qualifier : 'aws-accelerator') + '-pipeline-failed-status-topic', + masterKey: installerKey, + }); + + acceleratorFailedStatusTopic.grantPublish(pipeline.role); + + pipeline.notifyOn('AcceleratorPipelineFailureNotification', acceleratorFailedStatusTopic, { + events: [cdk.aws_codepipeline.PipelineNotificationEvents.PIPELINE_EXECUTION_FAILED], + }); + + acceleratorFailedStatusTopic + .metricNumberOfMessagesPublished() + .createAlarm(this, 'AcceleratorPipelineFailureAlarm', { + threshold: 1, + evaluationPeriods: 1, + datapointsToAlarm: 1, + treatMissingData: cdk.aws_cloudwatch.TreatMissingData.NOT_BREACHING, + alarmName: props.qualifier ? props.qualifier + '-pipeline-failed-alarm' : 'AwsAcceleratorFailedAlarm', + alarmDescription: 'AWS Accelerator pipeline failure alarm, created by accelerator', + }); + } + } + + private createToolkitStage(props: { + actionName: string; + command: string; + stage?: string; + runOrder?: number; + }): codepipeline_actions.CodeBuildAction { + let cdkOptions; + if ( + props.command === AcceleratorToolkitCommand.BOOTSTRAP.toString() || + props.command === AcceleratorToolkitCommand.DIFF.toString() + ) { + cdkOptions = props.command; + } else { + cdkOptions = `${props.command} --stage ${props.stage}`; + } + + const environmentVariables: { + [name: string]: cdk.aws_codebuild.BuildEnvironmentVariable; + } = { + CDK_OPTIONS: { + type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: cdkOptions, + }, + CONFIG_COMMIT_ID: { + type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: '#{Config-Vars.CommitId}', + }, + }; + + if (props.stage) { + environmentVariables['ACCELERATOR_STAGE'] = { + type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: props.stage ?? '', + }; + } + + return new codepipeline_actions.CodeBuildAction({ + actionName: props.actionName, + runOrder: props.runOrder, + project: this.toolkitProject, + input: this.buildOutput, + extraInputs: [this.configRepoArtifact], + role: this.pipelineRole, + environmentVariables, + }); + } +} diff --git a/source/packages/@aws-accelerator/accelerator/lib/stacks/accelerator-stack.ts b/source/packages/@aws-accelerator/accelerator/lib/stacks/accelerator-stack.ts new file mode 100644 index 000000000..84714d465 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/lib/stacks/accelerator-stack.ts @@ -0,0 +1,174 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { + AccountsConfig, + DeploymentTargets, + GlobalConfig, + IamConfig, + NetworkConfig, + OrganizationConfig, + SecurityConfig, + ShareTargets, +} from '@aws-accelerator/config'; +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import { Logger } from '../logger'; +import { version } from '../../../../../package.json'; + +export interface AcceleratorStackProps extends cdk.StackProps { + readonly configDirPath: string; + readonly accountsConfig: AccountsConfig; + readonly globalConfig: GlobalConfig; + readonly iamConfig: IamConfig; + readonly networkConfig: NetworkConfig; + readonly organizationConfig: OrganizationConfig; + readonly securityConfig: SecurityConfig; + readonly partition: string; + readonly qualifier?: string; + readonly configCommitId?: string; +} + +export abstract class AcceleratorStack extends cdk.Stack { + protected props: AcceleratorStackProps; + + protected constructor(scope: Construct, id: string, props: AcceleratorStackProps) { + super(scope, id, props); + this.props = props; + + new cdk.aws_ssm.StringParameter(this, 'SsmParamStackId', { + parameterName: `/accelerator/${cdk.Stack.of(this).stackName}/stack-id`, + stringValue: cdk.Stack.of(this).stackId, + }); + + new cdk.aws_ssm.StringParameter(this, 'SsmParamAcceleratorVersion', { + parameterName: `/accelerator/${cdk.Stack.of(this).stackName}/version`, + stringValue: version, + }); + } + + protected isIncluded(deploymentTargets: DeploymentTargets): boolean { + // Explicit Denies + if ( + this.isRegionExcluded(deploymentTargets.excludedRegions) || + this.isAccountExcluded(deploymentTargets.excludedAccounts) + ) { + return false; + } + + // Explicit Allows + if ( + this.isAccountIncluded(deploymentTargets.accounts) || + this.isOrganizationalUnitIncluded(deploymentTargets.organizationalUnits) + ) { + return true; + } + + // Implicit Deny + return false; + } + + protected getAccountIdsFromShareTarget(shareTargets: ShareTargets): string[] { + const accountIds: string[] = []; + + // Helper function to add an account id to the list + const addAccountId = (accountId: string) => { + if (!accountIds.includes(accountId)) { + accountIds.push(accountId); + } + }; + + for (const ou of shareTargets.organizationalUnits ?? []) { + // debug: processing ou + if (ou === 'Root') { + for (const account of this.props.accountsConfig.accountIds ?? []) { + // debug: accountId + addAccountId(account.accountId); + } + } else { + for (const account of [ + ...this.props.accountsConfig.mandatoryAccounts, + ...this.props.accountsConfig.workloadAccounts, + ]) { + if (ou === account.organizationalUnit) { + const accountId = this.props.accountsConfig.getAccountId(account.name); + // debug: accountId + addAccountId(accountId); + } + } + } + } + + for (const account of shareTargets.accounts ?? []) { + const accountId = this.props.accountsConfig.getAccountId(account); + // debug: accountId + addAccountId(accountId); + } + + return accountIds; + } + + protected isRegionExcluded(regions: string[]): boolean { + if (regions?.includes(cdk.Stack.of(this).region)) { + Logger.info(`[accelerator-stack] ${cdk.Stack.of(this).region} region explicitly excluded`); + return true; + } + return false; + } + + protected isAccountExcluded(accounts: string[]): boolean { + for (const account of accounts ?? []) { + if (cdk.Stack.of(this).account === this.props.accountsConfig.getAccountId(account)) { + Logger.info(`[accelerator-stack] ${account} account explicitly excluded`); + return true; + } + } + return false; + } + + protected isAccountIncluded(accounts: string[]): boolean { + for (const account of accounts ?? []) { + if (cdk.Stack.of(this).account === this.props.accountsConfig.getAccountId(account)) { + Logger.info(`[accelerator-stack] ${account} region explicitly included`); + return true; + } + } + return false; + } + + protected isOrganizationalUnitIncluded(organizationalUnits: string[]): boolean { + if (organizationalUnits) { + // If Root is specified, return right away + if (organizationalUnits.includes('Root')) { + return true; + } + + // Full list of all accounts + const accounts = [...this.props.accountsConfig.mandatoryAccounts, ...this.props.accountsConfig.workloadAccounts]; + + // Find the account with the matching ID + const account = accounts.find( + item => this.props.accountsConfig.getAccountId(item.name) === cdk.Stack.of(this).account, + ); + + if (account) { + if (organizationalUnits.indexOf(account.organizationalUnit) != -1) { + Logger.info(`[accelerator-stack] ${account.organizationalUnit} organizational unit explicitly included`); + return true; + } + } + } + + return false; + } +} diff --git a/source/packages/@aws-accelerator/accelerator/lib/stacks/accounts-stack.ts b/source/packages/@aws-accelerator/accelerator/lib/stacks/accounts-stack.ts new file mode 100644 index 000000000..90f09ea3b --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/lib/stacks/accounts-stack.ts @@ -0,0 +1,306 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { NagSuppressions } from 'cdk-nag'; +import { pascalCase } from 'change-case'; +import { Construct } from 'constructs'; +import * as path from 'path'; + +import { + Account, + EnablePolicyType, + Policy, + PolicyAttachment, + PolicyType, + PolicyTypeEnum, +} from '@aws-accelerator/constructs'; + +import { Logger } from '../logger'; +import { AcceleratorStack, AcceleratorStackProps } from './accelerator-stack'; +import { PrepareStack } from './prepare-stack'; + +export interface AccountsStackProps extends AcceleratorStackProps { + readonly configDirPath: string; +} + +let key: cdk.aws_kms.Key; +export class AccountsStack extends AcceleratorStack { + constructor(scope: Construct, id: string, props: AccountsStackProps) { + super(scope, id, props); + + Logger.debug(`[accounts-stack] Region: ${cdk.Stack.of(this).region}`); + + let globalRegion = 'us-east-1'; + if (props.partition === 'aws-us-gov') { + globalRegion = 'us-gov-west-1'; + } + + // Use existing management account key if in the home region + // otherwise create new kms key + if (props.globalConfig.homeRegion == cdk.Stack.of(this).region) { + const keyArn = cdk.aws_ssm.StringParameter.valueForStringParameter( + this, + PrepareStack.MANAGEMENT_KEY_ARN_PARAMETER_NAME, + ); + key = cdk.aws_kms.Key.fromKeyArn(this, 'ManagementKey', keyArn) as cdk.aws_kms.Key; + } else { + key = new cdk.aws_kms.Key(this, 'ManagementKey', { + alias: 'alias/accelerator/management/kms/key', + description: 'AWS Accelerator Management Account Kms Key', + enableKeyRotation: true, + removalPolicy: cdk.RemovalPolicy.RETAIN, + }); + + // Allow Accelerator Role to use the encryption key + key.addToResourcePolicy( + new cdk.aws_iam.PolicyStatement({ + sid: `Allow Accelerator Role in this account to use the encryption key`, + principals: [new cdk.aws_iam.AnyPrincipal()], + actions: ['kms:Encrypt', 'kms:Decrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*', 'kms:DescribeKey'], + resources: ['*'], + conditions: { + ArnLike: { + 'aws:PrincipalARN': [ + `arn:${cdk.Stack.of(this).partition}:iam::${cdk.Stack.of(this).account}:role/AWSAccelerator-*`, + ], + }, + }, + }), + ); + + // Allow Cloudwatch logs to use the encryption key + key.addToResourcePolicy( + new cdk.aws_iam.PolicyStatement({ + sid: `Allow Cloudwatch logs to use the encryption key`, + principals: [new cdk.aws_iam.ServicePrincipal(`logs.${cdk.Stack.of(this).region}.amazonaws.com`)], + actions: ['kms:Encrypt*', 'kms:Decrypt*', 'kms:ReEncrypt*', 'kms:GenerateDataKey*', 'kms:Describe*'], + resources: ['*'], + conditions: { + ArnLike: { + 'kms:EncryptionContext:aws:logs:arn': `arn:${cdk.Stack.of(this).partition}:logs:${ + cdk.Stack.of(this).region + }:${cdk.Stack.of(this).account}:log-group:*`, + }, + }, + }), + ); + + new cdk.aws_ssm.StringParameter(this, 'AcceleratorManagementKmsArnParameter', { + parameterName: PrepareStack.MANAGEMENT_KEY_ARN_PARAMETER_NAME, + stringValue: key.keyArn, + }); + } + + // + // Global Organizations actions + // + if (globalRegion === cdk.Stack.of(this).region) { + if (props.organizationConfig.enable) { + const enablePolicyTypeScp = new EnablePolicyType(this, 'enablePolicyTypeScp', { + policyType: PolicyTypeEnum.SERVICE_CONTROL_POLICY, + kmsKey: key, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }); + + // Invite Accounts to Organization (GovCloud) + const accountMap: Map = new Map(); + for (const account of [...props.accountsConfig.mandatoryAccounts, ...props.accountsConfig.workloadAccounts]) { + Logger.info(`[accounts-stack] Ensure ${account.name} is part of the Organization`); + + const organizationAccount = new Account(this, pascalCase(`${account.name}OrganizationAccount`), { + accountId: props.accountsConfig.getAccountId(account.name), + assumeRoleName: props.globalConfig.managementAccountAccessRole, + kmsKey: key, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }); + + accountMap.set(account.name, organizationAccount); + } + + // Deploy SCPs + let quarantineScpId = ''; + for (const serviceControlPolicy of props.organizationConfig.serviceControlPolicies) { + Logger.info(`[accounts-stack] Adding service control policy (${serviceControlPolicy.name})`); + + const scp = new Policy(this, serviceControlPolicy.name, { + description: serviceControlPolicy.description, + name: serviceControlPolicy.name, + path: path.join(props.configDirPath, serviceControlPolicy.policy), + type: PolicyType.SERVICE_CONTROL_POLICY, + kmsKey: key, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + acceleratorPrefix: 'AWSAccelerator', + managementAccountAccessRole: props.globalConfig.managementAccountAccessRole, + }); + scp.node.addDependency(enablePolicyTypeScp); + + if ( + serviceControlPolicy.name == props.organizationConfig.quarantineNewAccounts?.scpPolicyName && + props.partition == 'aws' + ) { + new cdk.aws_ssm.StringParameter(this, pascalCase(`SsmParam${scp.name}ScpPolicyId`), { + parameterName: `/accelerator/organizations/scp/${scp.name}/id`, + stringValue: scp.id, + }); + quarantineScpId = scp.id; + } + + for (const organizationalUnit of serviceControlPolicy.deploymentTargets.organizationalUnits ?? []) { + Logger.info( + `[accounts-stack] Attaching service control policy (${serviceControlPolicy.name}) to organizational unit (${organizationalUnit})`, + ); + + new PolicyAttachment(this, pascalCase(`Attach_${scp.name}_${organizationalUnit}`), { + policyId: scp.id, + targetId: props.organizationConfig.getOrganizationalUnitId(organizationalUnit), + type: PolicyType.SERVICE_CONTROL_POLICY, + kmsKey: key, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }); + } + + for (const account of serviceControlPolicy.deploymentTargets.accounts ?? []) { + const policyAttachment = new PolicyAttachment(this, pascalCase(`Attach_${scp.name}_${account}`), { + policyId: scp.id, + targetId: props.accountsConfig.getAccountId(account), + type: PolicyType.SERVICE_CONTROL_POLICY, + kmsKey: key, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }); + + // Add dependency to ensure that account is part of the OU before + // attempting to add the SCP + const organizationAccount = accountMap.get(account); + if (organizationAccount) { + policyAttachment.node.addDependency(organizationAccount); + } + } + } + + if (props.organizationConfig.quarantineNewAccounts?.enable === true && props.partition == 'aws') { + // Create resources to attach quarantine scp to + // new accounts created in organizations + Logger.info(`[accounts-stack] Creating resources to quarantine new accounts`); + const orgPolicyRead = new cdk.aws_iam.PolicyStatement({ + sid: 'OrgRead', + effect: cdk.aws_iam.Effect.ALLOW, + actions: ['organizations:ListPolicies', 'organizations:DescribeCreateAccountStatus'], + resources: ['*'], + }); + + const orgPolicyWrite = new cdk.aws_iam.PolicyStatement({ + sid: 'OrgWrite', + effect: cdk.aws_iam.Effect.ALLOW, + actions: ['organizations:AttachPolicy'], + resources: [ + `arn:${ + this.partition + }:organizations::${props.accountsConfig.getManagementAccountId()}:policy/o-*/service_control_policy/${quarantineScpId}`, + `arn:${this.partition}:organizations::${props.accountsConfig.getManagementAccountId()}:account/o-*/*`, + ], + }); + + Logger.info(`[accounts-stack] Creating function to attach quarantine scp to accounts`); + const attachQuarantineFunction = new cdk.aws_lambda.Function(this, 'AttachQuarantineScpFunction', { + code: cdk.aws_lambda.Code.fromAsset(path.join(__dirname, '../lambdas/attach-quarantine-scp/dist')), + runtime: cdk.aws_lambda.Runtime.NODEJS_14_X, + handler: 'index.handler', + description: 'Lambda function to attach quarantine scp to new accounts', + timeout: cdk.Duration.minutes(5), + environment: { SCP_POLICY_NAME: props.organizationConfig.quarantineNewAccounts?.scpPolicyName ?? '' }, + environmentEncryption: key, + initialPolicy: [orgPolicyRead, orgPolicyWrite], + }); + + // AwsSolutions-IAM4: The IAM user, role, or group uses AWS managed policies + NagSuppressions.addResourceSuppressionsByPath( + this, + `${this.stackName}/AttachQuarantineScpFunction/ServiceRole/Resource`, + [ + { + id: 'AwsSolutions-IAM4', + reason: 'AWS Custom resource provider framework-role created by cdk.', + }, + ], + ); + + // AwsSolutions-IAM5: The IAM entity contains wildcard permissions + NagSuppressions.addResourceSuppressionsByPath( + this, + `${this.stackName}/AttachQuarantineScpFunction/ServiceRole/DefaultPolicy/Resource`, + [ + { + id: 'AwsSolutions-IAM5', + reason: 'Allows only specific policy.', + }, + ], + ); + + const createAccountEventRule = new cdk.aws_events.Rule(this, 'CreateAccountRule', { + eventPattern: { + source: ['aws.organizations'], + detailType: ['AWS API Call via CloudTrail'], + detail: { + eventSource: ['organizations.amazonaws.com'], + eventName: ['CreateAccount'], + }, + }, + description: 'Rule to notify when a new account is created.', + }); + + createAccountEventRule.addTarget( + new cdk.aws_events_targets.LambdaFunction(attachQuarantineFunction, { + maxEventAge: cdk.Duration.hours(4), + retryAttempts: 2, + }), + ); + + //If any GovCloud accounts are configured also + //watch for any GovCloudCreateAccount events + if (props.accountsConfig.anyGovCloudAccounts()) { + Logger.info( + `[accounts-stack] Creating EventBridge rule to attach quarantine scp to accounts when GovCloud is enabled`, + ); + const createGovCloudAccountEventRule = new cdk.aws_events.Rule(this, 'CreateGovCloudAccountRule', { + eventPattern: { + source: ['aws.organizations'], + detailType: ['AWS API Call via CloudTrail'], + detail: { + eventSource: ['organizations.amazonaws.com'], + eventName: ['CreateGovCloudAccount'], + }, + }, + description: 'Rule to notify when a new account is created using the create govcloud account api.', + }); + + createGovCloudAccountEventRule.addTarget( + new cdk.aws_events_targets.LambdaFunction(attachQuarantineFunction, { + maxEventAge: cdk.Duration.hours(4), + retryAttempts: 2, + }), + ); + } + + new cdk.aws_logs.LogGroup(this, `${attachQuarantineFunction.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${attachQuarantineFunction.functionName}`, + retention: props.globalConfig.cloudwatchLogRetentionInDays, + encryptionKey: key, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + } + } + } + Logger.info('[accounts-stack] Completed stack synthesis'); + } +} diff --git a/source/packages/@aws-accelerator/accelerator/lib/stacks/dependencies-stack.ts b/source/packages/@aws-accelerator/accelerator/lib/stacks/dependencies-stack.ts new file mode 100644 index 000000000..fefba9c6c --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/lib/stacks/dependencies-stack.ts @@ -0,0 +1,27 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as ssm from 'aws-cdk-lib/aws-ssm'; +import { Construct } from 'constructs'; +import { AcceleratorStack, AcceleratorStackProps } from './accelerator-stack'; + +export class DependenciesStack extends AcceleratorStack { + constructor(scope: Construct, id: string, props: AcceleratorStackProps) { + super(scope, id, props); + + new ssm.StringParameter(this, 'Parameter', { + parameterName: `/accelerator/dependencies-stack/dependencies`, + stringValue: 'value', + }); + } +} diff --git a/source/packages/@aws-accelerator/accelerator/lib/stacks/finalize-stack.ts b/source/packages/@aws-accelerator/accelerator/lib/stacks/finalize-stack.ts new file mode 100644 index 000000000..59fe56608 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/lib/stacks/finalize-stack.ts @@ -0,0 +1,56 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import { AcceleratorStack, AcceleratorStackProps } from './accelerator-stack'; +import { Logger } from '../logger'; +import { DetachQuarantineScp } from '../detach-quarantine-scp'; + +export class FinalizeStack extends AcceleratorStack { + public static readonly ACCELERATOR_MANAGEMENT_KEY_ARN_PARAMETER_NAME = '/accelerator/management/kms/key-arn'; + + constructor(scope: Construct, id: string, props: AcceleratorStackProps) { + super(scope, id, props); + + Logger.debug(`[finalize-stack] Region: ${cdk.Stack.of(this).region}`); + + let globalRegion = 'us-east-1'; + if (props.partition === 'aws-us-gov') { + globalRegion = 'us-gov-west-1'; + } + + if (globalRegion === cdk.Stack.of(this).region) { + Logger.debug(`[finalize-stack] Retrieving kms key`); + const keyArn = cdk.aws_ssm.StringParameter.valueForStringParameter(this, '/accelerator/management/kms/key-arn'); + const key = cdk.aws_kms.Key.fromKeyArn(this, 'Resource', keyArn!) as cdk.aws_kms.Key; + + if (props.organizationConfig.quarantineNewAccounts?.enable && props.partition == 'aws') { + Logger.debug(`[finalize-stack] Creating resources to detach quarantine scp`); + const policyId = cdk.aws_ssm.StringParameter.valueForStringParameter( + this, + `/accelerator/organizations/scp/${props.organizationConfig.quarantineNewAccounts?.scpPolicyName}/id`, + ); + + new DetachQuarantineScp(this, 'DetachQuarantineScp', { + scpPolicyId: policyId, + managementAccountId: props.accountsConfig.getManagementAccountId(), + partition: props.partition, + kmsKey: key, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }); + } + } + Logger.info('[finalize-stack] Completed stack synthesis'); + } +} diff --git a/source/packages/@aws-accelerator/accelerator/lib/stacks/key-stack.ts b/source/packages/@aws-accelerator/accelerator/lib/stacks/key-stack.ts new file mode 100644 index 000000000..a8317ef50 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/lib/stacks/key-stack.ts @@ -0,0 +1,163 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { NagSuppressions } from 'cdk-nag'; +import { Construct } from 'constructs'; +import { AcceleratorStack, AcceleratorStackProps } from './accelerator-stack'; +import { Organization } from '@aws-accelerator/constructs'; +import { Logger } from '../logger'; + +export class KeyStack extends AcceleratorStack { + public static readonly CROSS_ACCOUNT_ACCESS_ROLE_NAME = 'AWSAccelerator-CrossAccount-SsmParameter-Role'; + public static readonly ACCELERATOR_KEY_ARN_PARAMETER_NAME = '/accelerator/kms/key-arn'; + + constructor(scope: Construct, id: string, props: AcceleratorStackProps) { + super(scope, id, props); + + Logger.debug(`[key-stack] Region: ${cdk.Stack.of(this).region}`); + + const organizationId = props.organizationConfig.enable ? new Organization(this, 'Organization').id : ''; + + const key = new cdk.aws_kms.Key(this, 'AcceleratorKey', { + alias: 'alias/accelerator/kms/key', + description: 'AWS Accelerator Kms Key', + enableKeyRotation: true, + removalPolicy: cdk.RemovalPolicy.RETAIN, + }); + + if (props.organizationConfig.enable) { + // Allow Accelerator Role to use the encryption key + key.addToResourcePolicy( + new cdk.aws_iam.PolicyStatement({ + sid: `Allow Accelerator Role to use the encryption key`, + principals: [new cdk.aws_iam.AnyPrincipal()], + actions: ['kms:Encrypt', 'kms:Decrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*', 'kms:DescribeKey'], + resources: ['*'], + conditions: { + StringEquals: { + 'aws:PrincipalOrgID': organizationId, + }, + ArnLike: { + 'aws:PrincipalARN': [`arn:${cdk.Stack.of(this).partition}:iam::*:role/AWSAccelerator-*`], + }, + }, + }), + ); + } + + // Allow Cloudwatch logs to use the encryption key + key.addToResourcePolicy( + new cdk.aws_iam.PolicyStatement({ + sid: `Allow Cloudwatch logs to use the encryption key`, + principals: [new cdk.aws_iam.ServicePrincipal(`logs.${cdk.Stack.of(this).region}.amazonaws.com`)], + actions: ['kms:Encrypt*', 'kms:Decrypt*', 'kms:ReEncrypt*', 'kms:GenerateDataKey*', 'kms:Describe*'], + resources: ['*'], + conditions: { + ArnLike: { + 'kms:EncryptionContext:aws:logs:arn': `arn:aws:logs:${cdk.Stack.of(this).region}:*:log-group:*`, + }, + }, + }), + ); + + // Add all services we want to allow usage + const allowedServicePrincipals: { name: string; principal: string }[] = [ + { name: 'Sns', principal: 'sns.amazonaws.com' }, + { name: 'Lambda', principal: 'lambda.amazonaws.com' }, + { name: 'Cloudwatch', principal: 'cloudwatch.amazonaws.com' }, + // Add similar objects for any other service principal needs access to this key + ]; + if (props.securityConfig.centralSecurityServices.macie.enable) { + allowedServicePrincipals.push({ name: 'Macie', principal: 'macie.amazonaws.com' }); + } + if (props.securityConfig.centralSecurityServices.guardduty.enable) { + allowedServicePrincipals.push({ name: 'Guardduty', principal: 'guardduty.amazonaws.com' }); + } + + allowedServicePrincipals!.forEach(item => { + key.addToResourcePolicy( + new cdk.aws_iam.PolicyStatement({ + sid: `Allow ${item.name} service to use the encryption key`, + principals: [new cdk.aws_iam.ServicePrincipal(item.principal)], + actions: ['kms:Encrypt', 'kms:Decrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*', 'kms:DescribeKey'], + resources: ['*'], + }), + ); + }); + + new cdk.aws_ssm.StringParameter(this, 'AcceleratorKmsArnParameter', { + parameterName: '/accelerator/kms/key-arn', + stringValue: key.keyArn, + }); + + // IAM Role to get access to accelerator organization level SSM parameters + // Only create this role in the home region stack + if (cdk.Stack.of(this).region === props.globalConfig.homeRegion && props.organizationConfig.enable) { + new cdk.aws_iam.Role(this, 'CrossAccountAcceleratorSsmParamAccessRole', { + roleName: KeyStack.CROSS_ACCOUNT_ACCESS_ROLE_NAME, + assumedBy: new cdk.aws_iam.OrganizationPrincipal(organizationId), + inlinePolicies: { + default: new cdk.aws_iam.PolicyDocument({ + statements: [ + new cdk.aws_iam.PolicyStatement({ + effect: cdk.aws_iam.Effect.ALLOW, + actions: ['ssm:GetParameters', 'ssm:GetParameter'], + resources: [ + `arn:${cdk.Stack.of(this).partition}:ssm:*:${ + cdk.Stack.of(this).account + }:parameter/accelerator/kms/key-arn`, + ], + conditions: { + StringEquals: { + 'aws:PrincipalOrgID': organizationId, + }, + ArnLike: { + 'aws:PrincipalARN': [`arn:${cdk.Stack.of(this).partition}:iam::*:role/AWSAccelerator-*`], + }, + }, + }), + new cdk.aws_iam.PolicyStatement({ + effect: cdk.aws_iam.Effect.ALLOW, + actions: ['ssm:DescribeParameters'], + resources: ['*'], + conditions: { + StringEquals: { + 'aws:PrincipalOrgID': organizationId, + }, + ArnLike: { + 'aws:PrincipalARN': [`arn:${cdk.Stack.of(this).partition}:iam::*:role/AWSAccelerator-*`], + }, + }, + }), + ], + }), + }, + }); + + // AwsSolutions-IAM5: The IAM entity contains wildcard permissions and does not have a cdk_nag + // rule suppression with evidence for this permission. + NagSuppressions.addResourceSuppressionsByPath( + this, + `${this.stackName}/CrossAccountAcceleratorSsmParamAccessRole/Resource`, + [ + { + id: 'AwsSolutions-IAM5', + reason: + 'This policy is required to give access to ssm parameters in every region where accelerator deployed. Various accelerator roles need permission to describe SSM parameters.', + }, + ], + ); + } + } +} diff --git a/source/packages/@aws-accelerator/accelerator/lib/stacks/logging-stack.ts b/source/packages/@aws-accelerator/accelerator/lib/stacks/logging-stack.ts new file mode 100644 index 000000000..1a164fe38 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/lib/stacks/logging-stack.ts @@ -0,0 +1,356 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { NagSuppressions } from 'cdk-nag'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import { Construct } from 'constructs'; +import { AcceleratorElbRootAccounts } from '../accelerator'; + +import { + Bucket, + BucketEncryptionType, + CentralLogsBucket, + KeyLookup, + Organization, + S3PublicAccessBlock, + SsmSessionManagerSettings, +} from '@aws-accelerator/constructs'; + +import { LifecycleRule } from '@aws-accelerator/constructs/lib/aws-s3/bucket'; +import { AcceleratorStack, AcceleratorStackProps } from './accelerator-stack'; +import { KeyStack } from './key-stack'; + +export class LoggingStack extends AcceleratorStack { + constructor(scope: Construct, id: string, props: AcceleratorStackProps) { + super(scope, id, props); + + const key = new KeyLookup(this, 'AcceleratorKeyLookup', { + accountId: props.accountsConfig.getAuditAccountId(), + roleName: KeyStack.CROSS_ACCOUNT_ACCESS_ROLE_NAME, + keyArnParameterName: KeyStack.ACCELERATOR_KEY_ARN_PARAMETER_NAME, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }).getKey(); + + let organizationId: string | undefined = undefined; + if (props.organizationConfig.enable) { + const organization = new Organization(this, 'Organization'); + organizationId = organization.id; + } + + // + // Block Public Access; S3 is global, only need to call in home region. This is done in the + // logging-stack instead of the security-stack since initial buckets are created in this stack. + // + if ( + cdk.Stack.of(this).region === props.globalConfig.homeRegion && + !this.isAccountExcluded(props.securityConfig.centralSecurityServices.s3PublicAccessBlock.excludeAccounts ?? []) + ) { + if (props.securityConfig.centralSecurityServices.s3PublicAccessBlock.enable) { + new S3PublicAccessBlock(this, 'S3PublicAccessBlock', { + blockPublicAcls: true, + blockPublicPolicy: true, + ignorePublicAcls: true, + restrictPublicBuckets: true, + accountId: cdk.Stack.of(this).account, + kmsKey: key, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }); + } + } + + // + // Create S3 Bucket for Access Logs - this is required + // + const lifecycleRules: LifecycleRule[] = []; + for (const lifecycleRule of props.globalConfig.logging.accessLogBucket?.lifecycleRules ?? []) { + const noncurrentVersionTransitions = []; + for (const noncurrentVersionTransition of lifecycleRule.noncurrentVersionTransitions) { + noncurrentVersionTransitions.push({ + storageClass: noncurrentVersionTransition.storageClass, + transitionAfter: noncurrentVersionTransition.transitionAfter, + }); + } + const transitions = []; + for (const transition of lifecycleRule.transitions) { + transitions.push({ + storageClass: transition.storageClass, + transitionAfter: transition.transitionAfter, + }); + } + const rule: LifecycleRule = { + abortIncompleteMultipartUploadAfter: lifecycleRule.abortIncompleteMultipartUpload, + enabled: lifecycleRule.enabled, + expiration: lifecycleRule.expiration, + expiredObjectDeleteMarker: lifecycleRule.expiredObjectDeleteMarker, + id: lifecycleRule.id, + noncurrentVersionExpiration: lifecycleRule.noncurrentVersionExpiration, + noncurrentVersionTransitions, + transitions, + }; + lifecycleRules.push(rule); + } + + const serverAccessLogsBucket = new Bucket(this, 'AccessLogsBucket', { + encryptionType: BucketEncryptionType.SSE_S3, // Server access logging does not support SSE-KMS + s3BucketName: `aws-accelerator-s3-access-logs-${cdk.Stack.of(this).account}-${cdk.Stack.of(this).region}`, + lifecycleRules, + }); + + // AwsSolutions-S1: The S3 Bucket has server access logs disabled. + NagSuppressions.addResourceSuppressionsByPath(this, `${this.stackName}/AccessLogsBucket/Resource/Resource`, [ + { + id: 'AwsSolutions-S1', + reason: 'AccessLogsBucket has server access logs disabled till the task for access logging completed.', + }, + ]); + + serverAccessLogsBucket.getS3Bucket().addToResourcePolicy( + new iam.PolicyStatement({ + sid: 'Allow write access for logging service principal', + effect: iam.Effect.ALLOW, + actions: ['s3:PutObject'], + principals: [new iam.ServicePrincipal('logging.s3.amazonaws.com')], + resources: [serverAccessLogsBucket.getS3Bucket().arnForObjects('*')], + conditions: { + StringEquals: { + 'aws:SourceAccount': cdk.Stack.of(this).account, + }, + }, + }), + ); + + // AwsSolutions-S1: The S3 Bucket has server access logs disabled. + NagSuppressions.addResourceSuppressionsByPath(this, `${this.stackName}/AccessLogsBucket/Resource/Resource`, [ + { + id: 'AwsSolutions-S1', + reason: 'AccessLogsBucket has server access logs disabled till the task for access logging completed.', + }, + ]); + + /** + * Create S3 Bucket for ELB Access Logs, this is created in log archive account + * For ELB to write access logs bucket is needed to have SSE-S3 server-side encryption + */ + if (cdk.Stack.of(this).account === props.accountsConfig.getLogArchiveAccountId()) { + const elbAccessLogsBucket = new Bucket(this, 'ElbAccessLogsBucket', { + encryptionType: BucketEncryptionType.SSE_S3, // Server access logging does not support SSE-KMS + s3BucketName: `aws-accelerator-elb-access-logs-${cdk.Stack.of(this).account}-${cdk.Stack.of(this).region}`, + }); + + const policies = [ + new cdk.aws_iam.PolicyStatement({ + sid: 'Allow get acl access for SSM principal', + effect: iam.Effect.ALLOW, + actions: ['s3:GetBucketAcl'], + principals: [new iam.ServicePrincipal('ssm.amazonaws.com')], + resources: [`${elbAccessLogsBucket.getS3Bucket().bucketArn}`], + }), + new cdk.aws_iam.PolicyStatement({ + sid: 'Allow write access for ELB Account principal', + effect: iam.Effect.ALLOW, + actions: ['s3:PutObject'], + principals: [new iam.AccountPrincipal(AcceleratorElbRootAccounts[cdk.Stack.of(this).region])], + resources: [`${elbAccessLogsBucket.getS3Bucket().bucketArn}/*`], + }), + new cdk.aws_iam.PolicyStatement({ + sid: 'Allow write access for delivery logging service principal', + effect: iam.Effect.ALLOW, + actions: ['s3:PutObject'], + principals: [new iam.ServicePrincipal('delivery.logs.amazonaws.com')], + resources: [`${elbAccessLogsBucket.getS3Bucket().bucketArn}/*`], + conditions: { + StringEquals: { + 's3:x-amz-acl': 'bucket-owner-full-control', + }, + }, + }), + new cdk.aws_iam.PolicyStatement({ + sid: 'Allow read bucket ACL access for delivery logging service principal', + effect: iam.Effect.ALLOW, + actions: ['s3:GetBucketAcl'], + principals: [new iam.ServicePrincipal('delivery.logs.amazonaws.com')], + resources: [`${elbAccessLogsBucket.getS3Bucket().bucketArn}`], + }), + ]; + + policies.forEach(item => { + elbAccessLogsBucket.getS3Bucket().addToResourcePolicy(item); + }); + + if (organizationId) { + elbAccessLogsBucket.getS3Bucket().addToResourcePolicy( + new cdk.aws_iam.PolicyStatement({ + sid: 'Allow Organization principals to use of the bucket', + effect: cdk.aws_iam.Effect.ALLOW, + actions: ['s3:GetBucketLocation', 's3:PutObject'], + principals: [new cdk.aws_iam.AnyPrincipal()], + resources: [ + `${elbAccessLogsBucket.getS3Bucket().bucketArn}`, + `${elbAccessLogsBucket.getS3Bucket().bucketArn}/*`, + ], + conditions: { + StringEquals: { + 'aws:PrincipalOrgID': organizationId, + }, + }, + }), + ); + } + + // AwsSolutions-S1: The S3 Bucket has server access logs disabled. + NagSuppressions.addResourceSuppressionsByPath(this, `${this.stackName}/ElbAccessLogsBucket/Resource/Resource`, [ + { + id: 'AwsSolutions-S1', + reason: 'ElbAccessLogsBucket has server access logs disabled till the task for access logging completed.', + }, + ]); + } + + // + // Create Central Logs Bucket - This is done only in the home region of the log-archive account. + // This is the destination bucket for all logs such as AWS CloudTrail, AWS Config, and VPC Flow + // Logs. Addition logs can also be sent to this bucket through AWS CloudWatch Logs, such as + // application logs, OS logs, or server logs. + // + // + if ( + cdk.Stack.of(this).region === props.globalConfig.homeRegion && + cdk.Stack.of(this).account === props.accountsConfig.getLogArchiveAccountId() + ) { + const lifecycleRules: LifecycleRule[] = []; + for (const lifecycleRule of props.globalConfig.logging.accessLogBucket?.lifecycleRules ?? []) { + const noncurrentVersionTransitions = []; + for (const noncurrentVersionTransition of lifecycleRule.noncurrentVersionTransitions) { + noncurrentVersionTransitions.push({ + storageClass: noncurrentVersionTransition.storageClass, + transitionAfter: noncurrentVersionTransition.transitionAfter, + }); + } + const transitions = []; + for (const transition of lifecycleRule.transitions) { + transitions.push({ + storageClass: transition.storageClass, + transitionAfter: transition.transitionAfter, + }); + } + const rule: LifecycleRule = { + abortIncompleteMultipartUploadAfter: lifecycleRule.abortIncompleteMultipartUpload, + enabled: lifecycleRule.enabled, + expiration: lifecycleRule.expiration, + expiredObjectDeleteMarker: lifecycleRule.expiredObjectDeleteMarker, + id: lifecycleRule.id, + noncurrentVersionExpiration: lifecycleRule.noncurrentVersionExpiration, + noncurrentVersionTransitions, + transitions, + }; + lifecycleRules.push(rule); + } + + new CentralLogsBucket(this, 'CentralLogsBucket', { + s3BucketName: `aws-accelerator-central-logs-${props.accountsConfig.getLogArchiveAccountId()}-${ + props.globalConfig.homeRegion + }`, + serverAccessLogsBucket: serverAccessLogsBucket, + kmsAliasName: 'alias/accelerator/central-logs/s3', + kmsDescription: 'AWS Accelerator Central Logs Bucket CMK', + organizationId, + lifecycleRules, + }); + + // AwsSolutions-IAM5: The IAM entity contains wildcard permissions and does not have a cdk_nag rule suppression with evidence for those permission. + // rule suppression with evidence for this permission. + NagSuppressions.addResourceSuppressionsByPath( + this, + `${this.stackName}/CentralLogsBucket/CrossAccountCentralBucketKMSArnSsmParamAccessRole/Resource`, + [ + { + id: 'AwsSolutions-IAM5', + reason: 'Central logs bucket arn SSM parameter needs access from other accounts', + }, + ], + ); + } + + if (props.securityConfig.centralSecurityServices.ebsDefaultVolumeEncryption.enable) { + // create service linked role for autoscaling + // if ebs default encryption enabled and using a customer master key + new iam.CfnServiceLinkedRole(this, 'AutoScalingServiceLinkedRole', { + awsServiceName: 'autoscaling.amazonaws.com', + description: + 'Default Service-Linked Role enables access to AWS Services and Resources used or managed by Auto Scaling', + }); + } + + if ( + props.globalConfig.logging.sessionManager.sendToCloudWatchLogs || + props.globalConfig.logging.sessionManager.sendToS3 + ) { + let centralLogBucket: cdk.aws_s3.IBucket | undefined; + if (props.globalConfig.logging.sessionManager.sendToS3) { + centralLogBucket = cdk.aws_s3.Bucket.fromBucketName( + this, + 'AcceleratorLogsBucket', + `aws-accelerator-s3-access-logs-${props.accountsConfig.getLogArchiveAccountId()}-${ + cdk.Stack.of(this).region + }`, + ); + } + + // Set up Session Manager Logging + if ( + !this.isAccountExcluded(props.globalConfig.logging.sessionManager.excludeAccounts ?? []) || + !this.isRegionExcluded(props.globalConfig.logging.sessionManager.excludeRegions ?? []) + ) { + new SsmSessionManagerSettings(this, 'SsmSessionManagerSettings', { + s3BucketName: centralLogBucket ? centralLogBucket.bucketName : undefined, + s3KeyPrefix: `AWSSessionManager/${cdk.Aws.ACCOUNT_ID}`, + s3BucketKeyArn: centralLogBucket ? centralLogBucket.bucketArn : undefined, + sendToCloudWatchLogs: props.globalConfig.logging.sessionManager.sendToCloudWatchLogs, + sendToS3: props.globalConfig.logging.sessionManager.sendToS3, + cloudWatchEncryptionEnabled: + props.partition !== 'aws-us-gov' && props.globalConfig.logging.sessionManager.sendToCloudWatchLogs, + kmsKey: key, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }); + + // AwsSolutions-IAM5: The IAM entity contains wildcard permissions and does not have a cdk_nag rule suppression with evidence for those permission. + // rule suppression with evidence for this permission. + NagSuppressions.addResourceSuppressionsByPath( + this, + `${this.stackName}/SsmSessionManagerSettings/SessionManagerEC2Policy/Resource`, + [ + { + id: 'AwsSolutions-IAM5', + reason: + 'Policy needed access to all S3 objects for the account to put objects into the access log bucket', + }, + ], + ); + + // AwsSolutions-IAM4: The IAM user, role, or group uses AWS managed policies. + // rule suppression with evidence for this permission. + NagSuppressions.addResourceSuppressionsByPath( + this, + `${this.stackName}/SsmSessionManagerSettings/SessionManagerEC2Role/Resource`, + [ + { + id: 'AwsSolutions-IAM4', + reason: 'Create an IAM managed Policy for users to be able to use Session Manager with KMS encryption', + }, + ], + ); + } + } + } +} diff --git a/source/packages/@aws-accelerator/accelerator/lib/stacks/network-associations-stack.ts b/source/packages/@aws-accelerator/accelerator/lib/stacks/network-associations-stack.ts new file mode 100644 index 000000000..d958db519 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/lib/stacks/network-associations-stack.ts @@ -0,0 +1,624 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import * as ssm from 'aws-cdk-lib/aws-ssm'; +import { pascalCase } from 'change-case'; +import { Construct } from 'constructs'; + +import { VpcConfig } from '@aws-accelerator/config'; +import { + AssociateHostedZones, + IResourceShareItem, + KeyLookup, + QueryLoggingConfigAssociation, + ResolverFirewallRuleGroupAssociation, + ResolverRuleAssociation, + ResourceShare, + ResourceShareItem, + ResourceShareOwner, + SsmParameter, + SsmParameterType, + TransitGatewayAttachment, + TransitGatewayRouteTableAssociation, + TransitGatewayRouteTablePropagation, + TransitGatewayStaticRoute, + VpcPeering, +} from '@aws-accelerator/constructs'; + +import { Logger } from '../logger'; +import { AcceleratorStack, AcceleratorStackProps } from './accelerator-stack'; +import { KeyStack } from './key-stack'; + +interface Peering { + name: string; + requester: VpcConfig; + accepter: VpcConfig; + tags: cdk.CfnTag[] | undefined; +} + +export class NetworkAssociationsStack extends AcceleratorStack { + private key: cdk.aws_kms.Key; + private logRetention: number; + constructor(scope: Construct, id: string, props: AcceleratorStackProps) { + super(scope, id, props); + + this.key = new KeyLookup(this, 'AcceleratorKeyLookup', { + accountId: props.accountsConfig.getAuditAccountId(), + roleName: KeyStack.CROSS_ACCOUNT_ACCESS_ROLE_NAME, + keyArnParameterName: KeyStack.ACCELERATOR_KEY_ARN_PARAMETER_NAME, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }).getKey(); + this.logRetention = props.globalConfig.cloudwatchLogRetentionInDays; + + // Build Transit Gateway Maps + const transitGateways = new Map(); + const transitGatewayRouteTables = new Map(); + const transitGatewayAttachments = new Map(); + for (const tgwItem of props.networkConfig.transitGateways ?? []) { + const accountId = props.accountsConfig.getAccountId(tgwItem.account); + if (accountId === cdk.Stack.of(this).account && tgwItem.region == cdk.Stack.of(this).region) { + const transitGatewayId = ssm.StringParameter.valueForStringParameter( + this, + `/accelerator/network/transitGateways/${tgwItem.name}/id`, + ); + transitGateways.set(tgwItem.name, transitGatewayId); + + for (const routeTableItem of tgwItem.routeTables ?? []) { + const transitGatewayRouteTableId = ssm.StringParameter.valueForStringParameter( + this, + `/accelerator/network/transitGateways/${tgwItem.name}/routeTables/${routeTableItem.name}/id`, + ); + const key = `${tgwItem.name}_${routeTableItem.name}`; + transitGatewayRouteTables.set(key, transitGatewayRouteTableId); + } + } + } + + for (const vpcItem of props.networkConfig.vpcs ?? []) { + if (vpcItem.region == cdk.Stack.of(this).region) { + for (const tgwAttachmentItem of vpcItem.transitGatewayAttachments ?? []) { + const accountId = props.accountsConfig.getAccountId(tgwAttachmentItem.transitGateway.account); + if (accountId === cdk.Stack.of(this).account) { + const owningAccountId = props.accountsConfig.getAccountId(vpcItem.account); + + // Get the Transit Gateway ID + const transitGatewayId = transitGateways.get(tgwAttachmentItem.transitGateway.name); + if (transitGatewayId === undefined) { + throw new Error(`Transit Gateway ${tgwAttachmentItem.transitGateway.name} not found`); + } + + // Get the Transit Gateway Attachment ID + let transitGatewayAttachmentId; + const key = `${vpcItem.account}_${vpcItem.name}`; + if (accountId === owningAccountId) { + Logger.info( + `[network-associations-stack] Update route tables for attachment ${tgwAttachmentItem.name} from local account ${owningAccountId}`, + ); + transitGatewayAttachmentId = ssm.StringParameter.valueForStringParameter( + this, + `/accelerator/network/vpc/${vpcItem.name}/transitGatewayAttachment/${tgwAttachmentItem.name}/id`, + ); + transitGatewayAttachments.set(key, transitGatewayAttachmentId); + } else { + Logger.info( + `[network-associations-stack] Update route tables for attachment ${tgwAttachmentItem.name} from external account ${owningAccountId}`, + ); + + const transitGatewayAttachment = TransitGatewayAttachment.fromLookup( + this, + pascalCase(`${tgwAttachmentItem.name}VpcTransitGatewayAttachment`), + { + name: tgwAttachmentItem.name, + owningAccountId, + transitGatewayId, + roleName: `AWSAccelerator-DescribeTgwAttachRole-${cdk.Stack.of(this).region}`, + kmsKey: this.key, + logRetentionInDays: this.logRetention, + }, + ); + // Build Transit Gateway Attachment Map + transitGatewayAttachmentId = transitGatewayAttachment.transitGatewayAttachmentId; + transitGatewayAttachments.set(key, transitGatewayAttachmentId); + } + + // Evaluating TGW Routes + + // Evaluate Route Table Associations + for (const routeTableItem of tgwAttachmentItem.routeTableAssociations ?? []) { + const key = `${tgwAttachmentItem.transitGateway.name}_${routeTableItem}`; + + const transitGatewayRouteTableId = transitGatewayRouteTables.get(key); + if (transitGatewayRouteTableId === undefined) { + throw new Error(`Transit Gateway Route Table ${key} not found`); + } + + new TransitGatewayRouteTableAssociation( + this, + `${pascalCase(tgwAttachmentItem.name)}${pascalCase(routeTableItem)}Association`, + { + transitGatewayAttachmentId, + transitGatewayRouteTableId, + }, + ); + } + + // Evaluate Route Table Propagations + for (const routeTableItem of tgwAttachmentItem.routeTablePropagations ?? []) { + const key = `${tgwAttachmentItem.transitGateway.name}_${routeTableItem}`; + + const transitGatewayRouteTableId = transitGatewayRouteTables.get(key); + if (transitGatewayRouteTableId === undefined) { + throw new Error(`Transit Gateway Route Table ${key} not found`); + } + + new TransitGatewayRouteTablePropagation( + this, + `${pascalCase(tgwAttachmentItem.name)}${pascalCase(routeTableItem)}Propagation`, + { + transitGatewayAttachmentId, + transitGatewayRouteTableId, + }, + ); + } + } + } + } + } + + // Evaluate Transit Gateway Static Routes + for (const tgwItem of props.networkConfig.transitGateways ?? []) { + if ( + cdk.Stack.of(this).account === props.accountsConfig.getAccountId(tgwItem.account) && + cdk.Stack.of(this).region === tgwItem.region + ) { + for (const routeTableItem of tgwItem.routeTables ?? []) { + for (const routeItem of routeTableItem.routes ?? []) { + // Throw exception when a blackhole route and a VPC attachment is presented. + if (routeItem.blackhole && routeItem.attachment) { + throw new Error('Cannot specify blackhole route and an attachment!'); + } + // Build a static route when a route is being blackholed. + if (routeItem.blackhole) { + Logger.info( + `[network-associations-stack] Adding blackhole route ${routeItem.destinationCidrBlock} to TGW route table ${routeTableItem.name} for TGW ${tgwItem.name} in account: ${tgwItem.account}`, + ); + new TransitGatewayStaticRoute( + this, + `${routeTableItem.name}-${routeItem.destinationCidrBlock}-blackhole`, + { + transitGatewayRouteTableId: cdk.aws_ssm.StringParameter.valueForStringParameter( + this, + `/accelerator/network/transitGateways/${tgwItem.name}/routeTables/${routeTableItem.name}/id`, + ), + blackhole: routeItem.blackhole, + destinationCidrBlock: routeItem.destinationCidrBlock, + }, + ); + } else if (routeItem.attachment) { + // Get TGW attachment ID + const transitGatewayAttachmentId = transitGatewayAttachments.get( + `${routeItem.attachment.account}_${routeItem.attachment.vpcName}`, + ); + + if (transitGatewayAttachmentId) { + Logger.info( + `[network-associations-stack] Adding static route ${routeItem.destinationCidrBlock} to VPC attachment ${routeItem.attachment.vpcName} in account ${routeItem.attachment.account} to route table ${routeTableItem.name} for TGW ${tgwItem.name}`, + ); + + new TransitGatewayStaticRoute( + this, + `${routeTableItem.name}-${routeItem.destinationCidrBlock}-${routeItem.attachment.vpcName}-${routeItem.attachment.account}`, + { + transitGatewayRouteTableId: cdk.aws_ssm.StringParameter.valueForStringParameter( + this, + `/accelerator/network/transitGateways/${tgwItem.name}/routeTables/${routeTableItem.name}/id`, + ), + destinationCidrBlock: routeItem.destinationCidrBlock, + transitGatewayAttachmentId: transitGatewayAttachmentId, + }, + ); + } else { + throw new Error( + `[network-associations-stack] Unable to locate transit gateway attachment ID for route table item ${routeTableItem.name}`, + ); + } + } + } + } + } + } + + // + // Get the Central Endpoints VPC, there should only be one. + // Only care if the VPC is defined within this account and region + // + let centralEndpointVpc = undefined; + const centralEndpointVpcs = props.networkConfig.vpcs.filter( + item => + item.interfaceEndpoints?.central && + props.accountsConfig.getAccountId(item.account) === cdk.Stack.of(this).account && + item.region === cdk.Stack.of(this).region, + ); + if (centralEndpointVpcs.length > 1) { + throw new Error(`multiple (${centralEndpointVpcs.length}) central endpoint vpcs detected, should only be one`); + } + centralEndpointVpc = centralEndpointVpcs[0]; + + if (centralEndpointVpc) { + Logger.info( + '[network-associations-stack] Central endpoints VPC detected, share private hosted zone with member VPCs', + ); + + // Generate list of accounts with VPCs that needed to set up share + const accountIds: string[] = []; + for (const vpcItem of props.networkConfig.vpcs ?? []) { + if (vpcItem.region === cdk.Stack.of(this).region && vpcItem.useCentralEndpoints) { + const accountId = props.accountsConfig.getAccountId(vpcItem.account); + if (!accountIds.includes(accountId)) { + accountIds.push(accountId); + } + } + } + + // Create list of hosted zone ids from SSM Params + const hostedZoneIds: string[] = []; + for (const endpointItem of centralEndpointVpc.interfaceEndpoints?.endpoints ?? []) { + const hostedZoneId = cdk.aws_ssm.StringParameter.valueForStringParameter( + this, + `/accelerator/network/vpc/${centralEndpointVpc.name}/route53/hostedZone/${endpointItem.service}/id`, + ); + hostedZoneIds.push(hostedZoneId); + } + + // Custom resource to associate hosted zones + new AssociateHostedZones(this, 'AssociateHostedZones', { + accountIds, + hostedZoneIds, + hostedZoneAccountId: cdk.Stack.of(this).account, + roleName: `AWSAccelerator-EnableCentralEndpointsRole-${cdk.Stack.of(this).region}`, + tagFilters: [ + { + key: 'accelerator:use-central-endpoints', + value: 'true', + }, + { + key: 'accelerator:central-endpoints-account-id', + value: props.accountsConfig.getAccountId(centralEndpointVpc.account), + }, + ], + kmsKey: this.key, + logRetentionInDays: this.logRetention, + }); + } + + // + // Route 53 Resolver associations + // + const dnsFirewallMap = new Map(); + const queryLogMap = new Map(); + const resolverRuleMap = new Map(); + const centralNetworkConfig = props.networkConfig.centralNetworkServices; + + for (const vpcItem of props.networkConfig.vpcs ?? []) { + const accountId = props.accountsConfig.getAccountId(vpcItem.account); + // Only care about VPCs to be created in the current account and region + if (accountId === cdk.Stack.of(this).account && vpcItem.region == cdk.Stack.of(this).region) { + // DNS firewall rule group associations + for (const firewallItem of vpcItem.dnsFirewallRuleGroups ?? []) { + // Get VPC ID + const vpcId = cdk.aws_ssm.StringParameter.valueForStringParameter( + this, + `/accelerator/network/vpc/${vpcItem.name}/id`, + ); + + // Skip lookup if already added to map + if (dnsFirewallMap.has(firewallItem.name)) { + continue; + } + + if (centralNetworkConfig?.delegatedAdminAccount) { + const owningAccountId = props.accountsConfig.getAccountId(centralNetworkConfig.delegatedAdminAccount); + + // Get SSM parameter if this is the owning account + if (owningAccountId === cdk.Stack.of(this).account) { + const ruleId = cdk.aws_ssm.StringParameter.valueForStringParameter( + this, + `/accelerator/network/route53Resolver/firewall/ruleGroups/${firewallItem.name}/id`, + ); + dnsFirewallMap.set(firewallItem.name, ruleId); + } else { + // Get ID from the resource share + const ruleId = this.getResourceShare( + vpcItem.name, + `${firewallItem.name}_ResolverFirewallRuleGroupShare`, + 'route53resolver:FirewallRuleGroup', + owningAccountId, + ).resourceShareItemId; + dnsFirewallMap.set(firewallItem.name, ruleId); + } + } + + // Create association + if (!dnsFirewallMap.get(firewallItem.name)) { + throw new Error( + `[network-associations-stack] Could not find existing DNS firewall rule group ${firewallItem.name}`, + ); + } + Logger.info( + `[network-associations-stack] Add DNS firewall rule group ${firewallItem.name} to ${vpcItem.name}`, + ); + + new ResolverFirewallRuleGroupAssociation( + this, + pascalCase(`${vpcItem.name}${firewallItem.name}RuleGroupAssociation`), + { + firewallRuleGroupId: dnsFirewallMap.get(firewallItem.name)!, + priority: firewallItem.priority, + vpcId: vpcId, + mutationProtection: firewallItem.mutationProtection, + tags: firewallItem.tags, + }, + ); + } + + // + // Route 53 query logging configuration associations + // + for (const configItem of vpcItem.queryLogs ?? []) { + // Get VPC ID + const vpcId = cdk.aws_ssm.StringParameter.valueForStringParameter( + this, + `/accelerator/network/vpc/${vpcItem.name}/id`, + ); + + // Skip lookup if already added to map + if (queryLogMap.has(configItem)) { + continue; + } + + if (centralNetworkConfig?.delegatedAdminAccount) { + const owningAccountId = props.accountsConfig.getAccountId(centralNetworkConfig.delegatedAdminAccount); + + // Determine query log destination(s) + const configNames: string[] = []; + if (centralNetworkConfig.route53Resolver?.queryLogs?.destinations.includes('s3')) { + configNames.push(`${configItem}-s3`); + } + if (centralNetworkConfig.route53Resolver?.queryLogs?.destinations.includes('cloud-watch-logs')) { + configNames.push(`${configItem}-cwl`); + } + + // Get SSM parameter if this is the owning account + for (const nameItem of configNames) { + if (owningAccountId === cdk.Stack.of(this).account) { + const configId = cdk.aws_ssm.StringParameter.valueForStringParameter( + this, + `/accelerator/network/route53Resolver/queryLogConfigs/${nameItem}/id`, + ); + queryLogMap.set(nameItem, configId); + } else { + // Get ID from the resource share + const configId = this.getResourceShare( + vpcItem.name, + `${nameItem}_QueryLogConfigShare`, + 'route53resolver:ResolverQueryLogConfig', + owningAccountId, + ).resourceShareItemId; + queryLogMap.set(nameItem, configId); + } + } + + // Create association + for (const nameItem of configNames) { + if (!queryLogMap.get(nameItem)) { + throw new Error( + `[network-associations-stack] Could not find existing DNS query log config ${nameItem}`, + ); + } + Logger.info(`[network-associations-stack] Add DNS query log config ${nameItem} to ${vpcItem.name}`); + new QueryLoggingConfigAssociation(this, pascalCase(`${vpcItem.name}${nameItem}QueryLogAssociation`), { + resolverQueryLogConfigId: queryLogMap.get(nameItem), + vpcId: vpcId, + }); + } + } + } + + // + // Route 53 resolver rule associations + // + for (const ruleItem of vpcItem.resolverRules ?? []) { + // Get VPC ID + const vpcId = cdk.aws_ssm.StringParameter.valueForStringParameter( + this, + `/accelerator/network/vpc/${vpcItem.name}/id`, + ); + + // Skip lookup if already added to map + if (resolverRuleMap.has(ruleItem)) { + continue; + } + + if (centralNetworkConfig?.delegatedAdminAccount) { + const owningAccountId = props.accountsConfig.getAccountId(centralNetworkConfig.delegatedAdminAccount); + + // Get SSM parameter if this is the owning account + if (owningAccountId === cdk.Stack.of(this).account) { + const ruleId = cdk.aws_ssm.StringParameter.valueForStringParameter( + this, + `/accelerator/network/route53Resolver/rules/${ruleItem}/id`, + ); + resolverRuleMap.set(ruleItem, ruleId); + } else { + // Get ID from the resource share + const ruleId = this.getResourceShare( + vpcItem.name, + `${ruleItem}_ResolverRule`, + 'route53resolver:ResolverRule', + owningAccountId, + ).resourceShareItemId; + resolverRuleMap.set(ruleItem, ruleId); + } + } + + // Create association + if (!resolverRuleMap.get(ruleItem)) { + throw new Error(`[network-associations-stack] Could not find existing Route 53 Resolver rule ${ruleItem}`); + } + Logger.info(`[network-associations-stack] Add Route 53 Resolver rule ${ruleItem} to ${vpcItem.name}`); + new ResolverRuleAssociation(this, pascalCase(`${vpcItem.name}${ruleItem}RuleAssociation`), { + resolverRuleId: resolverRuleMap.get(ruleItem)!, + vpcId: vpcId, + }); + } + } + } + + // + // Check for VPC peering connections + // + const peeringList: Peering[] = []; + for (const peering of props.networkConfig.vpcPeering ?? []) { + // Check to ensure only two VPCs are defined + if (peering.vpcs.length > 2) { + throw new Error(`[network-vpc-stack] VPC peering connection ${peering.name} has more than two VPCs defined`); + } + + // Get requester and accepter VPC configurations + const requesterVpc = props.networkConfig.vpcs.filter(item => item.name === peering.vpcs[0]); + const accepterVpc = props.networkConfig.vpcs.filter(item => item.name === peering.vpcs[1]); + + if (requesterVpc.length === 1 && accepterVpc.length === 1) { + const requesterAccountId = props.accountsConfig.getAccountId(requesterVpc[0].account); + + // Check if requester VPC is in this account and region + if (cdk.Stack.of(this).account === requesterAccountId && cdk.Stack.of(this).region === requesterVpc[0].region) { + peeringList.push({ + name: peering.name, + requester: requesterVpc[0], + accepter: accepterVpc[0], + tags: peering.tags, + }); + } + } + } + + // Create VPC peering connections + for (const peering of peeringList ?? []) { + // Get account IDs + const requesterAccountId = props.accountsConfig.getAccountId(peering.requester.account); + const accepterAccountId = props.accountsConfig.getAccountId(peering.accepter.account); + + // Get SSM parameters + const requesterVpcId = cdk.aws_ssm.StringParameter.valueForStringParameter( + this, + `/accelerator/network/vpc/${peering.requester.name}/id`, + ); + + let accepterVpcId: string; + let accepterRoleName: string | undefined = undefined; + if (requesterAccountId !== accepterAccountId) { + accepterVpcId = new SsmParameter(this, pascalCase(`SsmParamLookup${peering.name}`), { + region: peering.accepter.region, + partition: cdk.Stack.of(this).partition, + parameter: { + name: `/accelerator/network/vpc/${peering.accepter.name}/id`, + accountId: accepterAccountId, + roleName: `AWSAccelerator-VpcPeeringRole-${peering.accepter.region}`, + }, + invokingAccountID: cdk.Stack.of(this).account, + type: SsmParameterType.GET, + }).value; + + accepterRoleName = `AWSAccelerator-VpcPeeringRole-${peering.accepter.region}`; + } else { + accepterVpcId = cdk.aws_ssm.StringParameter.valueForStringParameter( + this, + `/accelerator/network/vpc/${peering.accepter.name}/id`, + ); + } + + // Create VPC peering + Logger.info( + `[network-associations-stack] Create VPC peering ${peering.name} between ${peering.requester.name} and ${peering.accepter.name}`, + ); + const vpcPeering = new VpcPeering(this, `${peering.name}VpcPeering`, { + name: peering.name, + peerOwnerId: accepterAccountId, + peerRegion: peering.accepter.region, + peerVpcId: accepterVpcId, + peerRoleName: accepterRoleName, + vpcId: requesterVpcId, + tags: peering.tags ?? [], + }); + new cdk.aws_ssm.StringParameter(this, pascalCase(`SsmParam${pascalCase(peering.name)}VpcPeering`), { + parameterName: `/accelerator/network/vpcPeering/${peering.name}/id`, + stringValue: vpcPeering.peeringId, + }); + + // Put cross-account SSM parameter if necessary + if (requesterAccountId !== accepterAccountId) { + new SsmParameter(this, pascalCase(`CrossAcctSsmParam${pascalCase(peering.name)}VpcPeering`), { + region: peering.accepter.region, + partition: cdk.Stack.of(this).partition, + parameter: { + name: `/accelerator/network/vpcPeering/${peering.name}/id`, + accountId: accepterAccountId, + roleName: `AWSAccelerator-VpcPeeringRole-${peering.accepter.region}`, + value: vpcPeering.peeringId, + }, + invokingAccountID: cdk.Stack.of(this).account, + type: SsmParameterType.PUT, + }); + } + } + + Logger.info('[network-associations-stack] Completed stack synthesis'); + } + + /** + * Get the resource ID from a RAM share. + * + * @param vpcName + * @param resourceShareName + * @param itemType + * @param owningAccountId + */ + private getResourceShare( + vpcName: string, + resourceShareName: string, + itemType: string, + owningAccountId: string, + ): IResourceShareItem { + // Generate a logical ID + const resourceName = resourceShareName.split('_')[0]; + const logicalId = `${vpcName}${resourceName}${itemType.split(':')[1]}`; + + // Lookup resource share + const resourceShare = ResourceShare.fromLookup(this, pascalCase(`${logicalId}Share`), { + resourceShareOwner: ResourceShareOwner.OTHER_ACCOUNTS, + resourceShareName: resourceShareName, + owningAccountId, + }); + + // Represents the item shared by RAM + const item = ResourceShareItem.fromLookup(this, pascalCase(`${logicalId}`), { + resourceShare, + resourceShareItemType: itemType, + kmsKey: this.key, + logRetentionInDays: this.logRetention, + }); + return item; + } +} diff --git a/source/packages/@aws-accelerator/accelerator/lib/stacks/network-prep-stack.ts b/source/packages/@aws-accelerator/accelerator/lib/stacks/network-prep-stack.ts new file mode 100644 index 000000000..4ae2b9031 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/lib/stacks/network-prep-stack.ts @@ -0,0 +1,465 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import * as ssm from 'aws-cdk-lib/aws-ssm'; +import { pascalCase } from 'change-case'; +import { Construct } from 'constructs'; +import * as path from 'path'; + +import { + AccountsConfig, + DnsFirewallRuleGroupConfig, + DnsQueryLogsConfig, + NfwFirewallPolicyConfig, + NfwRuleGroupConfig, + OrganizationConfig, + TransitGatewayConfig, +} from '@aws-accelerator/config'; +import { + FirewallPolicyProperty, + KeyLookup, + NetworkFirewallPolicy, + NetworkFirewallRuleGroup, + Organization, + QueryLoggingConfig, + ResolverFirewallDomainList, + ResolverFirewallDomainListType, + ResolverFirewallRuleGroup, + ResourceShare, + TransitGateway, + TransitGatewayRouteTable, +} from '@aws-accelerator/constructs'; + +import { Logger } from '../logger'; +import { AcceleratorStack, AcceleratorStackProps } from './accelerator-stack'; +import { KeyStack } from './key-stack'; + +interface ResolverFirewallRuleProps { + action: string; + firewallDomainListId: string; + priority: number; + blockOverrideDnsType?: string; + blockOverrideDomain?: string; + blockOverrideTtl?: number; + blockResponse?: string; +} + +type ResourceShareType = + | DnsFirewallRuleGroupConfig + | DnsQueryLogsConfig + | NfwRuleGroupConfig + | NfwFirewallPolicyConfig + | TransitGatewayConfig; + +export class NetworkPrepStack extends AcceleratorStack { + private accountsConfig: AccountsConfig; + private orgConfig: OrganizationConfig; + private key: cdk.aws_kms.Key; + private logRetention: number; + + constructor(scope: Construct, id: string, props: AcceleratorStackProps) { + super(scope, id, props); + + // Set private properties + this.accountsConfig = props.accountsConfig; + this.orgConfig = props.organizationConfig; + this.logRetention = props.globalConfig.cloudwatchLogRetentionInDays; + + this.key = new KeyLookup(this, 'AcceleratorKeyLookup', { + accountId: props.accountsConfig.getAuditAccountId(), + roleName: KeyStack.CROSS_ACCOUNT_ACCESS_ROLE_NAME, + keyArnParameterName: KeyStack.ACCELERATOR_KEY_ARN_PARAMETER_NAME, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }).getKey(); + + // + // Generate Transit Gateways + // + for (const tgwItem of props.networkConfig.transitGateways ?? []) { + const accountId = this.accountsConfig.getAccountId(tgwItem.account); + if (accountId === cdk.Stack.of(this).account && tgwItem.region == cdk.Stack.of(this).region) { + Logger.info(`[network-prep-stack] Add Transit Gateway ${tgwItem.name}`); + + const tgw = new TransitGateway(this, pascalCase(`${tgwItem.name}TransitGateway`), { + name: tgwItem.name, + amazonSideAsn: tgwItem.asn, + autoAcceptSharedAttachments: tgwItem.autoAcceptSharingAttachments, + defaultRouteTableAssociation: tgwItem.defaultRouteTableAssociation, + defaultRouteTablePropagation: tgwItem.defaultRouteTablePropagation, + dnsSupport: tgwItem.dnsSupport, + vpnEcmpSupport: tgwItem.vpnEcmpSupport, + tags: tgwItem.tags, + }); + + new ssm.StringParameter(this, pascalCase(`SsmParam${tgwItem.name}TransitGatewayId`), { + parameterName: `/accelerator/network/transitGateways/${tgwItem.name}/id`, + stringValue: tgw.transitGatewayId, + }); + + for (const routeTableItem of tgwItem.routeTables ?? []) { + Logger.info(`[network-prep-stack] Add Transit Gateway Route Tables ${routeTableItem.name}`); + + const routeTable = new TransitGatewayRouteTable( + this, + pascalCase(`${routeTableItem.name}TransitGatewayRouteTable`), + { + transitGatewayId: tgw.transitGatewayId, + name: routeTableItem.name, + tags: routeTableItem.tags, + }, + ); + + new ssm.StringParameter( + this, + pascalCase(`SsmParam${tgwItem.name}${routeTableItem.name}TransitGatewayRouteTableId`), + { + parameterName: `/accelerator/network/transitGateways/${tgwItem.name}/routeTables/${routeTableItem.name}/id`, + stringValue: routeTable.id, + }, + ); + } + + if (tgwItem.shareTargets) { + Logger.info(`[network-prep-stack] Share transit gateway`); + this.addResourceShare(tgwItem, `${tgwItem.name}_TransitGatewayShare`, [tgw.transitGatewayArn]); + } + } + } + + // + // Central network services + // + if (props.networkConfig.centralNetworkServices) { + const centralConfig = props.networkConfig.centralNetworkServices; + const accountId = this.accountsConfig.getAccountId(centralConfig.delegatedAdminAccount); + + // + // DNS firewall + // + // Create and store domain lists first + const domainMap = new Map(); + + for (const firewallItem of centralConfig.route53Resolver?.firewallRuleGroups ?? []) { + const regions = firewallItem.regions.map(item => { + return item.toString(); + }); + + // Create regional rule groups in the delegated admin account + if (accountId === cdk.Stack.of(this).account && regions.includes(cdk.Stack.of(this).region)) { + for (const ruleItem of firewallItem.rules) { + let domainListType: ResolverFirewallDomainListType; + let filePath: string | undefined = undefined; + let listName: string; + // Check to ensure both types aren't defined + if (ruleItem.customDomainList && ruleItem.managedDomainList) { + throw new Error( + `[network-prep-stack] Only one of customDomainList or managedDomainList may be defined for ${ruleItem.name}`, + ); + } else if (ruleItem.customDomainList) { + domainListType = ResolverFirewallDomainListType.CUSTOM; + filePath = path.join(props.configDirPath, ruleItem.customDomainList); + try { + listName = ruleItem.customDomainList.split('/')[1].split('.')[0]; + if (!domainMap.has(listName)) { + Logger.info(`[network-prep-stack] Creating DNS firewall custom domain list ${listName}`); + } + } catch (e) { + throw new Error(`[network-prep-stack] Error creating DNS firewall domain list: ${e}`); + } + } else if (ruleItem.managedDomainList) { + domainListType = ResolverFirewallDomainListType.MANAGED; + listName = ruleItem.managedDomainList; + if (!domainMap.has(listName)) { + Logger.info(`[network-prep-stack] Looking up DNS firewall managed domain list ${listName}`); + } + } else { + throw new Error( + `[network-prep-stack] One of customDomainList or managedDomainList must be defined for ${ruleItem.name}`, + ); + } + + // Create or look up domain list + if (!domainMap.has(listName)) { + const domainList = new ResolverFirewallDomainList(this, pascalCase(`${listName}DomainList`), { + name: listName, + path: filePath, + tags: [], + type: domainListType, + kmsKey: this.key, + logRetentionInDays: this.logRetention, + }); + domainMap.set(listName, domainList.listId); + } + } + + // Build new rule list with domain list ID + const ruleList: ResolverFirewallRuleProps[] = []; + let listName: string; + for (const ruleItem of firewallItem.rules) { + // Check the type of domain list + if (ruleItem.customDomainList) { + try { + listName = ruleItem.customDomainList.split('/')[1].split('.')[0]; + } catch (e) { + throw new Error(`[network-prep-stack] Error parsing list name from ${ruleItem.customDomainList}`); + } + } else { + listName = ruleItem.managedDomainList!; + } + + // Create the DNS firewall rule list + + if (domainMap.get(listName)) { + if (ruleItem.action === 'BLOCK' && ruleItem.blockResponse === 'OVERRIDE') { + ruleList.push({ + action: ruleItem.action.toString(), + firewallDomainListId: domainMap.get(listName)!, + priority: ruleItem.priority, + blockOverrideDnsType: 'CNAME', + blockOverrideDomain: ruleItem.blockOverrideDomain, + blockOverrideTtl: ruleItem.blockOverrideTtl, + blockResponse: ruleItem.blockResponse, + }); + } else { + ruleList.push({ + action: ruleItem.action.toString(), + firewallDomainListId: domainMap.get(listName)!, + priority: ruleItem.priority, + blockResponse: ruleItem.blockResponse, + }); + } + } else { + throw new Error(`Domain list ${listName} not found in domain map`); + } + } + + Logger.info(`[network-prep-stack] Creating DNS firewall rule group ${firewallItem.name}`); + const ruleGroup = new ResolverFirewallRuleGroup(this, pascalCase(`${firewallItem.name}RuleGroup`), { + firewallRules: ruleList, + name: firewallItem.name, + tags: firewallItem.tags ?? [], + }); + new ssm.StringParameter(this, pascalCase(`SsmParam${firewallItem.name}RuleGroup`), { + parameterName: `/accelerator/network/route53Resolver/firewall/ruleGroups/${firewallItem.name}/id`, + stringValue: ruleGroup.groupId, + }); + + if (firewallItem.shareTargets) { + Logger.info(`[network-prep-stack] Share DNS firewall rule group ${firewallItem.name}`); + this.addResourceShare(firewallItem, `${firewallItem.name}_ResolverFirewallRuleGroupShare`, [ + ruleGroup.groupArn, + ]); + } + } + } + + // + // Route53 Resolver query log configuration + // + if (centralConfig.route53Resolver?.queryLogs) { + // Create query log configurations only in the delegated admin account + if (accountId === cdk.Stack.of(this).account) { + const logItem = centralConfig.route53Resolver.queryLogs; + + if (logItem.destinations.includes('s3')) { + Logger.info(`[network-prep-stack] Create DNS query log ${logItem.name}-s3 for central S3 destination`); + const centralLogsBucket = cdk.aws_s3.Bucket.fromBucketName( + this, + 'CentralLogsBucket', + `aws-accelerator-central-logs-${props.accountsConfig.getLogArchiveAccountId()}-${ + props.globalConfig.homeRegion + }`, + ); + + const s3QueryLogConfig = new QueryLoggingConfig(this, pascalCase(`${logItem.name}S3QueryLogConfig`), { + destination: centralLogsBucket, + name: `${logItem.name}-s3`, + }); + new ssm.StringParameter(this, pascalCase(`SsmParam${logItem.name}S3QueryLogConfig`), { + parameterName: `/accelerator/network/route53Resolver/queryLogConfigs/${logItem.name}-s3/id`, + stringValue: s3QueryLogConfig.logId, + }); + + if (logItem.shareTargets) { + Logger.info(`[network-prep-stack] Share DNS query log config ${logItem.name}-s3`); + this.addResourceShare(logItem, `${logItem.name}-s3_QueryLogConfigShare`, [s3QueryLogConfig.logArn]); + } + } + + if (logItem.destinations.includes('cloud-watch-logs')) { + Logger.info( + `[network-prep-stack] Create DNS query log ${logItem.name}-cwl for central CloudWatch logs destination`, + ); + const organization = new Organization(this, 'Organization'); + + const logGroup = new cdk.aws_logs.LogGroup(this, 'QueryLogsLogGroup', { + encryptionKey: this.key, + retention: this.logRetention, + }); + + const cwlQueryLogConfig = new QueryLoggingConfig(this, pascalCase(`${logItem.name}CwlQueryLogConfig`), { + destination: logGroup, + name: `${logItem.name}-cwl`, + organizationId: organization.id, + }); + new ssm.StringParameter(this, pascalCase(`SsmParam${logItem.name}CwlQueryLogConfig`), { + parameterName: `/accelerator/network/route53Resolver/queryLogConfigs/${logItem.name}-cwl/id`, + stringValue: cwlQueryLogConfig.logId, + }); + + if (logItem.shareTargets) { + Logger.info(`[network-prep-stack] Share DNS query log config ${logItem.name}-cwl`); + this.addResourceShare(logItem, `${logItem.name}-cwl_QueryLogConfigShare`, [cwlQueryLogConfig.logArn]); + } + } + } + } + + // + // Network Firewall rule groups + // + const ruleMap = new Map(); + + for (const ruleItem of centralConfig.networkFirewall?.rules ?? []) { + const regions = ruleItem.regions.map(item => { + return item.toString(); + }); + + // Create regional rule groups in the delegated admin account + if (accountId === cdk.Stack.of(this).account && regions.includes(cdk.Stack.of(this).region)) { + Logger.info(`[network-prep-stack] Create network firewall rule group ${ruleItem.name}`); + const rule = new NetworkFirewallRuleGroup(this, pascalCase(`${ruleItem.name}NetworkFirewallRuleGroup`), { + capacity: ruleItem.capacity, + name: ruleItem.name, + type: ruleItem.type, + description: ruleItem.description, + ruleGroup: ruleItem.ruleGroup, + tags: ruleItem.tags ?? [], + }); + ruleMap.set(ruleItem.name, rule.groupArn); + new ssm.StringParameter(this, pascalCase(`SsmParam${ruleItem.name}NetworkFirewallRuleGroup`), { + parameterName: `/accelerator/network/networkFirewall/ruleGroups/${ruleItem.name}/arn`, + stringValue: rule.groupArn, + }); + + if (ruleItem.shareTargets) { + Logger.info(`[network-prep-stack] Share Network Firewall rule group ${ruleItem.name}`); + this.addResourceShare(ruleItem, `${ruleItem.name}_NetworkFirewallRuleGroupShare`, [rule.groupArn]); + } + } + } + + // + // Network Firewall policies + // + for (const policyItem of centralConfig.networkFirewall?.policies ?? []) { + const regions = policyItem.regions.map(item => { + return item.toString(); + }); + + // Create regional rule groups in the delegated admin account + if (accountId === cdk.Stack.of(this).account && regions.includes(cdk.Stack.of(this).region)) { + // Store rule group references to associate with policy + const statefulGroups = []; + const statelessGroups = []; + + for (const group of policyItem.firewallPolicy.statefulRuleGroups ?? []) { + if (ruleMap.has(group.name)) { + statefulGroups.push({ priority: group.priority, resourceArn: ruleMap.get(group.name)! }); + } else { + throw new Error(`[network-prep-stack] Rule group ${group.name} not found in rule map`); + } + } + + for (const group of policyItem.firewallPolicy.statelessRuleGroups ?? []) { + if (ruleMap.has(group.name)) { + statelessGroups.push({ priority: group.priority, resourceArn: ruleMap.get(group.name)! }); + } else { + throw new Error(`[network-prep-stack] Rule group ${group.name} not found in rule map`); + } + } + + // Create new firewall policy object with rule group references + const firewallPolicy: FirewallPolicyProperty = { + statelessDefaultActions: policyItem.firewallPolicy.statelessDefaultActions, + statelessFragmentDefaultActions: policyItem.firewallPolicy.statelessFragmentDefaultActions, + statefulDefaultActions: policyItem.firewallPolicy.statefulDefaultActions, + statefulEngineOptions: policyItem.firewallPolicy.statefulEngineOptions, + statefulRuleGroupReferences: statefulGroups, + statelessCustomActions: policyItem.firewallPolicy.statelessCustomActions, + statelessRuleGroupReferences: statelessGroups, + }; + + // Instantiate firewall policy construct + Logger.info(`[network-prep-stack] Create network firewall policy ${policyItem.name}`); + const policy = new NetworkFirewallPolicy(this, pascalCase(`${policyItem.name}NetworkFirewallPolicy`), { + name: policyItem.name, + firewallPolicy: firewallPolicy, + description: policyItem.description, + tags: policyItem.tags ?? [], + }); + new ssm.StringParameter(this, pascalCase(`SsmParam${policyItem.name}NetworkFirewallPolicy`), { + parameterName: `/accelerator/network/networkFirewall/policies/${policyItem.name}/arn`, + stringValue: policy.policyArn, + }); + + if (policyItem.shareTargets) { + Logger.info(`[network-prep-stack] Share Network Firewall policy ${policyItem.name}`); + this.addResourceShare(policyItem, `${policyItem.name}_NetworkFirewallPolicyShare`, [policy.policyArn]); + } + } + } + } + + Logger.info('[network-prep-stack] Completed stack synthesis'); + } + + /** + * Add RAM resource shares to the stack. + * + * @param item + * @param resourceShareName + * @param resourceArns + */ + private addResourceShare(item: ResourceShareType, resourceShareName: string, resourceArns: string[]) { + // Build a list of principals to share to + const principals: string[] = []; + + // Loop through all the defined OUs + for (const ouItem of item.shareTargets?.organizationalUnits ?? []) { + let ouArn = this.orgConfig.getOrganizationalUnitArn(ouItem); + // AWS::RAM::ResourceShare expects the organizations ARN if + // sharing with the entire org (Root) + if (ouItem === 'Root') { + ouArn = ouArn.substring(0, ouArn.lastIndexOf('/')).replace('root', 'organization'); + } + Logger.info(`[network-prep-stack] Share ${resourceShareName} with Organizational Unit ${ouItem}: ${ouArn}`); + principals.push(ouArn); + } + + // Loop through all the defined accounts + for (const account of item.shareTargets?.accounts ?? []) { + const accountId = this.accountsConfig.getAccountId(account); + Logger.info(`[network-prep-stack] Share ${resourceShareName} with Account ${account}: ${accountId}`); + principals.push(accountId); + } + + // Create the Resource Share + new ResourceShare(this, `${pascalCase(resourceShareName)}ResourceShare`, { + name: resourceShareName, + principals, + resourceArns: resourceArns, + }); + } +} diff --git a/source/packages/@aws-accelerator/accelerator/lib/stacks/network-vpc-dns-stack.ts b/source/packages/@aws-accelerator/accelerator/lib/stacks/network-vpc-dns-stack.ts new file mode 100644 index 000000000..28fbeeef2 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/lib/stacks/network-vpc-dns-stack.ts @@ -0,0 +1,308 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { pascalCase } from 'change-case'; +import { Construct } from 'constructs'; + +import { + AccountsConfig, + OrganizationConfig, + ResolverEndpointConfig, + ResolverRuleConfig, + VpcConfig, +} from '@aws-accelerator/config'; +import { HostedZone, KeyLookup, RecordSet, ResolverRule, ResourceShare } from '@aws-accelerator/constructs'; + +import { Logger } from '../logger'; +import { AcceleratorStack, AcceleratorStackProps } from './accelerator-stack'; +import { KeyStack } from './key-stack'; + +type ResourceShareType = ResolverRuleConfig; + +export class NetworkVpcDnsStack extends AcceleratorStack { + private acceleratorKey: cdk.aws_kms.Key; + private accountsConfig: AccountsConfig; + private logRetention: number; + private orgConfig: OrganizationConfig; + + constructor(scope: Construct, id: string, props: AcceleratorStackProps) { + super(scope, id, props); + + // Set private properties + this.accountsConfig = props.accountsConfig; + this.logRetention = props.globalConfig.cloudwatchLogRetentionInDays; + this.orgConfig = props.organizationConfig; + + this.acceleratorKey = new KeyLookup(this, 'AcceleratorKeyLookup', { + accountId: props.accountsConfig.getAuditAccountId(), + roleName: KeyStack.CROSS_ACCOUNT_ACCESS_ROLE_NAME, + keyArnParameterName: KeyStack.ACCELERATOR_KEY_ARN_PARAMETER_NAME, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }).getKey(); + + // + // Store VPC IDs, interface endpoint DNS, and Route 53 resolver endpoints + // + const vpcMap = new Map(); + const endpointMap = new Map(); + const zoneMap = new Map(); + const resolverMap = new Map(); + for (const vpcItem of props.networkConfig.vpcs ?? []) { + const accountId = this.accountsConfig.getAccountId(vpcItem.account); + if (accountId === cdk.Stack.of(this).account && vpcItem.region === cdk.Stack.of(this).region) { + // Set VPC ID + const vpcId = cdk.aws_ssm.StringParameter.valueForStringParameter( + this, + `/accelerator/network/vpc/${vpcItem.name}/id`, + ); + vpcMap.set(vpcItem.name, vpcId); + + // Set interface endpoint DNS names + for (const endpointItem of vpcItem.interfaceEndpoints?.endpoints ?? []) { + const endpointDns = cdk.aws_ssm.StringParameter.valueForStringParameter( + this, + `/accelerator/network/vpc/${vpcItem.name}/endpoints/${endpointItem.service}/dns`, + ); + const zoneId = cdk.aws_ssm.StringParameter.valueForStringParameter( + this, + `/accelerator/network/vpc/${vpcItem.name}/endpoints/${endpointItem.service}/hostedZoneId`, + ); + endpointMap.set(`${vpcItem.name}_${endpointItem.service}`, endpointDns); + zoneMap.set(`${vpcItem.name}_${endpointItem.service}`, zoneId); + } + + // Set Route 53 resolver endpoints + if (props.networkConfig.centralNetworkServices?.route53Resolver?.endpoints) { + const endpoints = props.networkConfig.centralNetworkServices?.route53Resolver?.endpoints; + + for (const endpointItem of endpoints) { + // Only map endpoints for relevant VPCs + if (endpointItem.vpc === vpcItem.name) { + const endpointId = cdk.aws_ssm.StringParameter.valueForStringParameter( + this, + `/accelerator/network/route53Resolver/endpoints/${endpointItem.name}/id`, + ); + resolverMap.set(`${vpcItem.name}_${endpointItem.name}`, endpointId); + } + } + } + } + } + + // + // Create private hosted zones + // + for (const vpcItem of props.networkConfig.vpcs ?? []) { + const accountId = this.accountsConfig.getAccountId(vpcItem.account); + if (accountId === cdk.Stack.of(this).account && vpcItem.region === cdk.Stack.of(this).region) { + const vpcId = vpcMap.get(vpcItem.name); + + if (!vpcId) { + throw new Error(`[network-vpc-dns-stack] Unable to locate VPC ${vpcItem.name}`); + } + // Create private hosted zones + if (vpcItem.interfaceEndpoints) { + this.createHostedZones(vpcItem, vpcId, endpointMap, zoneMap); + } + + // + // Create resolver rules + // + if (props.networkConfig.centralNetworkServices?.route53Resolver?.endpoints) { + const endpoints = props.networkConfig.centralNetworkServices?.route53Resolver?.endpoints; + + for (const endpointItem of endpoints) { + if (endpointItem.vpc === vpcItem.name && endpointItem.type === 'OUTBOUND') { + this.createResolverRules(vpcItem, endpointItem, resolverMap); + } + } + } + } + } + Logger.info('[network-vpc-dns-stack] Completed stack synthesis'); + } + + /** + * Create private hosted zones + * + * @param vpcItem + * @param vpcId + * @param endpointMap + * @param zoneMap + */ + private createHostedZones( + vpcItem: VpcConfig, + vpcId: string, + endpointMap: Map, + zoneMap: Map, + ): void { + for (const endpointItem of vpcItem.interfaceEndpoints?.endpoints ?? []) { + // Create the private hosted zone + Logger.info( + `[network-vpc-dns-stack] Creating private hosted zone for VPC:${vpcItem.name} endpoint:${endpointItem.service}`, + ); + const hostedZoneName = HostedZone.getHostedZoneNameForService(endpointItem.service, cdk.Stack.of(this).region); + const hostedZone = new HostedZone( + this, + `${pascalCase(vpcItem.name)}Vpc${pascalCase(endpointItem.service)}EpHostedZone`, + { + hostedZoneName, + vpcId, + }, + ); + new cdk.aws_ssm.StringParameter( + this, + `SsmParam${pascalCase(vpcItem.name)}Vpc${pascalCase(endpointItem.service)}EpHostedZone`, + { + parameterName: `/accelerator/network/vpc/${vpcItem.name}/route53/hostedZone/${endpointItem.service}/id`, + stringValue: hostedZone.hostedZoneId, + }, + ); + + // Create the record set + let recordSetName = hostedZoneName; + const wildcardServices = ['ecr.dkr', 's3']; + if (wildcardServices.includes(endpointItem.service)) { + recordSetName = `*.${hostedZoneName}`; + } + + // Check mapping for DNS name + const endpointKey = `${vpcItem.name}_${endpointItem.service}`; + const dnsName = endpointMap.get(endpointKey); + const zoneId = zoneMap.get(endpointKey); + if (!dnsName) { + throw new Error( + `[network-vpc-dns-stack] Unable to locate DNS name for VPC:${vpcItem.name} endpoint:${endpointItem.service}`, + ); + } + if (!zoneId) { + throw new Error( + `[network-vpc-dns-stack] Unable to locate hosted zone ID for VPC:${vpcItem.name} endpoint:${endpointItem.service}`, + ); + } + + Logger.info( + `[network-vpc-dns-stack] Creating alias record for VPC:${vpcItem.name} endpoint:${endpointItem.service}`, + ); + new RecordSet(this, `${pascalCase(vpcItem.name)}Vpc${pascalCase(endpointItem.service)}EpRecordSet`, { + type: 'A', + name: recordSetName, + hostedZone: hostedZone, + dnsName: dnsName, + hostedZoneId: zoneId, + }); + } + } + + /** + * Create Route 53 resolver rules + * + * @param vpcItem + * @param endpointItem + * @param resolverMap + */ + private createResolverRules( + vpcItem: VpcConfig, + endpointItem: ResolverEndpointConfig, + resolverMap: Map, + ): void { + // Check for endpoint in map + const endpointKey = `${vpcItem.name}_${endpointItem.name}`; + const endpointId = resolverMap.get(endpointKey); + if (!endpointId) { + throw new Error( + `[network-vpc-dns-stack] Create resolver rule: unable to locate resolver endpoint ${endpointItem.name}`, + ); + } + + // Create rules + for (const ruleItem of endpointItem.rules ?? []) { + Logger.info( + `[network-vpc-dns-stack] Add Route 53 Resolver rule ${ruleItem.name} to endpoint ${endpointItem.name}`, + ); + + // Check whether there is an inbound endpoint target + let inboundTarget: string | undefined = undefined; + if (ruleItem.inboundEndpointTarget) { + const targetKey = `${vpcItem.name}_${ruleItem.inboundEndpointTarget}`; + inboundTarget = resolverMap.get(targetKey); + if (!inboundTarget) { + throw new Error( + `[network-vpc-dns-stack] Target inbound endpoint: ${ruleItem.inboundEndpointTarget} not found in endpoint map`, + ); + } + } + + // Create resolver rule and SSM parameter + const rule = new ResolverRule(this, `${pascalCase(endpointItem.name)}ResolverRule${pascalCase(ruleItem.name)}`, { + domainName: ruleItem.domainName, + name: ruleItem.name, + ruleType: ruleItem.ruleType, + resolverEndpointId: endpointId, + targetIps: ruleItem.targetIps, + tags: ruleItem.tags ?? [], + targetInbound: inboundTarget, + kmsKey: this.acceleratorKey, + logRetentionInDays: this.logRetention, + }); + new cdk.aws_ssm.StringParameter(this, pascalCase(`SsmParam${ruleItem.name}ResolverRule`), { + parameterName: `/accelerator/network/route53Resolver/rules/${ruleItem.name}/id`, + stringValue: rule.ruleId, + }); + + if (ruleItem.shareTargets) { + Logger.info(`[network-vpc-dns-stack] Share Route 53 Resolver rule ${ruleItem.name}`); + this.addResourceShare(ruleItem, `${ruleItem.name}_ResolverRule`, [rule.ruleArn]); + } + } + } + + /** + * Add RAM resource shares to the stack. + * + * @param item + * @param resourceShareName + * @param resourceArns + */ + private addResourceShare(item: ResourceShareType, resourceShareName: string, resourceArns: string[]): void { + // Build a list of principals to share to + const principals: string[] = []; + + // Loop through all the defined OUs + for (const ouItem of item.shareTargets?.organizationalUnits ?? []) { + let ouArn = this.orgConfig.getOrganizationalUnitArn(ouItem); + // AWS::RAM::ResourceShare expects the organizations ARN if + // sharing with the entire org (Root) + if (ouItem === 'Root') { + ouArn = ouArn.substring(0, ouArn.lastIndexOf('/')).replace('root', 'organization'); + } + Logger.info(`[network-vpc-dns-stack] Share ${resourceShareName} with Organizational Unit ${ouItem}: ${ouArn}`); + principals.push(ouArn); + } + + // Loop through all the defined accounts + for (const account of item.shareTargets?.accounts ?? []) { + const accountId = this.accountsConfig.getAccountId(account); + Logger.info(`[network-vpc-dns-stack] Share ${resourceShareName} with Account ${account}: ${accountId}`); + principals.push(accountId); + } + + // Create the Resource Share + new ResourceShare(this, `${pascalCase(resourceShareName)}ResourceShare`, { + name: resourceShareName, + principals, + resourceArns: resourceArns, + }); + } +} diff --git a/source/packages/@aws-accelerator/accelerator/lib/stacks/network-vpc-endpoints-stack.ts b/source/packages/@aws-accelerator/accelerator/lib/stacks/network-vpc-endpoints-stack.ts new file mode 100644 index 000000000..cf070163c --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/lib/stacks/network-vpc-endpoints-stack.ts @@ -0,0 +1,825 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { NagSuppressions } from 'cdk-nag'; +import { pascalCase } from 'change-case'; +import { Construct } from 'constructs'; +import * as fs from 'fs'; +import * as path from 'path'; + +import { + AccountsConfig, + GatewayEndpointServiceConfig, + GlobalConfig, + InterfaceEndpointServiceConfig, + NfwFirewallConfig, + ResolverEndpointConfig, + VpcConfig, +} from '@aws-accelerator/config'; +import { + IResourceShareItem, + KeyLookup, + NetworkFirewall, + ResolverEndpoint, + ResourceShare, + ResourceShareItem, + ResourceShareOwner, + SecurityGroup, + SecurityGroupEgressRuleProps, + SecurityGroupIngressRuleProps, + VpcEndpoint, +} from '@aws-accelerator/constructs'; + +import { Logger } from '../logger'; +import { AcceleratorStack, AcceleratorStackProps } from './accelerator-stack'; +import { KeyStack } from './key-stack'; + +export class NetworkVpcEndpointsStack extends AcceleratorStack { + private acceleratorKey: cdk.aws_kms.Key; + private accountsConfig: AccountsConfig; + private globalConfig: GlobalConfig; + private logRetention: number; + + constructor(scope: Construct, id: string, props: AcceleratorStackProps) { + super(scope, id, props); + + // Set private properties + this.accountsConfig = props.accountsConfig; + this.globalConfig = props.globalConfig; + this.logRetention = props.globalConfig.cloudwatchLogRetentionInDays; + + this.acceleratorKey = new KeyLookup(this, 'AcceleratorKeyLookup', { + accountId: props.accountsConfig.getAuditAccountId(), + roleName: KeyStack.CROSS_ACCOUNT_ACCESS_ROLE_NAME, + keyArnParameterName: KeyStack.ACCELERATOR_KEY_ARN_PARAMETER_NAME, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }).getKey(); + + // + // Store VPC, subnet, and route table IDs + // + const vpcMap = new Map(); + const subnetMap = new Map(); + const routeTableMap = new Map(); + for (const vpcItem of props.networkConfig.vpcs ?? []) { + const accountId = this.accountsConfig.getAccountId(vpcItem.account); + if (accountId === cdk.Stack.of(this).account && vpcItem.region === cdk.Stack.of(this).region) { + // Set VPC ID + const vpcId = cdk.aws_ssm.StringParameter.valueForStringParameter( + this, + `/accelerator/network/vpc/${vpcItem.name}/id`, + ); + vpcMap.set(vpcItem.name, vpcId); + + // Set subnet IDs + for (const subnetItem of vpcItem.subnets ?? []) { + const subnetId = cdk.aws_ssm.StringParameter.valueForStringParameter( + this, + `/accelerator/network/vpc/${vpcItem.name}/subnet/${subnetItem.name}/id`, + ); + subnetMap.set(`${vpcItem.name}_${subnetItem.name}`, subnetId); + } + + // Set route table IDs + for (const routeTableItem of vpcItem.routeTables ?? []) { + const routeTableId = cdk.aws_ssm.StringParameter.valueForStringParameter( + this, + `/accelerator/network/vpc/${vpcItem.name}/routeTable/${routeTableItem.name}/id`, + ); + routeTableMap.set(`${vpcItem.name}_${routeTableItem.name}`, routeTableId); + } + } + } + + // + // Iterate through VPCs in this account and region + // + const firewallMap = new Map(); + for (const vpcItem of props.networkConfig.vpcs ?? []) { + const accountId = this.accountsConfig.getAccountId(vpcItem.account); + if (accountId === cdk.Stack.of(this).account && vpcItem.region === cdk.Stack.of(this).region) { + const vpcId = vpcMap.get(vpcItem.name); + if (!vpcId) { + throw new Error(`[network-vpc-endpoints-stack] Unable to locate VPC ${vpcItem.name}`); + } + // + // Create VPC endpoints + // + if (vpcItem.gatewayEndpoints) { + this.createGatewayEndpoints(vpcItem, vpcId, routeTableMap); + } + + if (vpcItem.interfaceEndpoints) { + this.createInterfaceEndpoints(vpcItem, vpcId, subnetMap); + } + + // + // Create Network Firewalls + // + if (props.networkConfig.centralNetworkServices?.networkFirewall?.firewalls) { + const firewalls = props.networkConfig.centralNetworkServices.networkFirewall.firewalls; + + for (const firewallItem of firewalls) { + if (firewallItem.vpc === vpcItem.name) { + const firewallSubnets: string[] = []; + const delegatedAdminAccountId = this.accountsConfig.getAccountId( + props.networkConfig.centralNetworkServices.delegatedAdminAccount, + ); + let owningAccountId: string | undefined = undefined; + + // Check if this is not the delegated network admin account + if (delegatedAdminAccountId !== cdk.Stack.of(this).account) { + owningAccountId = delegatedAdminAccountId; + } + + // Check if VPC has matching subnets + for (const subnetItem of firewallItem.subnets) { + const subnetKey = `${firewallItem.vpc}_${subnetItem}`; + const subnetId = subnetMap.get(subnetKey); + if (subnetId) { + firewallSubnets.push(subnetId); + } else { + throw new Error( + `[network-vpc-endpoints-stack] Create Network Firewall: subnet ${subnetItem} not found in VPC ${firewallItem.vpc}`, + ); + } + } + + // Create firewall + if (firewallSubnets.length > 0) { + const nfw = this.createNetworkFirewall(firewallItem, vpcId, firewallSubnets, owningAccountId); + firewallMap.set(firewallItem.name, nfw); + } + } + } + } + + // + // Create endpoint routes + // + for (const routeTableItem of vpcItem.routeTables ?? []) { + // Check if endpoint routes exist + for (const routeTableEntryItem of routeTableItem.routes ?? []) { + const id = + pascalCase(`${vpcItem.name}Vpc`) + + pascalCase(`${routeTableItem.name}RouteTable`) + + pascalCase(routeTableEntryItem.name); + // + // Network Firewall routes + // + if (routeTableEntryItem.type === 'networkFirewall') { + const routeTableId = routeTableMap.get(`${vpcItem.name}_${routeTableItem.name}`); + + // Check if route table exists im map + if (!routeTableId) { + throw new Error( + `[network-vpc-endpoints-stack] Add Network Firewall route: unable to locate route table ${routeTableItem.name}`, + ); + } + + // Check for AZ input + if (!routeTableEntryItem.targetAvailabilityZone) { + throw new Error( + `[network-vpc-endpoints-stack] Network Firewall route table entry ${routeTableEntryItem.name} must specify a target availability zone`, + ); + } + + // Get Network Firewall and SSM parameter storing endpoint values + const firewall = firewallMap.get(routeTableEntryItem.target); + const endpointAz = `${cdk.Stack.of(this).region}${routeTableEntryItem.targetAvailabilityZone}`; + + if (!firewall) { + throw new Error( + `[network-vpc-endpoints-stack] Unable to locate Network Firewall ${routeTableEntryItem.target}`, + ); + } + // Add route + Logger.info( + `[network-vpc-endpoints-stack] Adding Network Firewall Route Table Entry ${routeTableEntryItem.name}`, + ); + const routeOptions = { + id: id, + destination: routeTableEntryItem.destination, + endpointAz: endpointAz, + firewallArn: firewall.firewallArn, + kmsKey: this.acceleratorKey, + logRetention: this.logRetention, + routeTableId: routeTableId, + }; + firewall.addNetworkFirewallRoute(routeOptions); + } + } + } + + // + // Create Route 53 Resolver Endpoints + // + if (props.networkConfig.centralNetworkServices?.route53Resolver?.endpoints) { + const delegatedAdminAccountId = this.accountsConfig.getAccountId( + props.networkConfig.centralNetworkServices.delegatedAdminAccount, + ); + const endpoints = props.networkConfig.centralNetworkServices?.route53Resolver?.endpoints; + + // Check if the VPC has matching subnets + for (const endpointItem of endpoints) { + if (vpcItem.name === endpointItem.vpc) { + const endpointSubnets: string[] = []; + + // Check if this is the delegated admin account + if (accountId !== delegatedAdminAccountId) { + throw new Error( + '[network-vpc-endpoints-stack] VPC for Route 53 Resolver endpoints must be located in the delegated network administrator account', + ); + } + + for (const subnetItem of endpointItem.subnets) { + const subnetKey = `${vpcItem.name}_${subnetItem}`; + const subnetId = subnetMap.get(subnetKey); + if (subnetId) { + endpointSubnets.push(subnetId); + } else { + throw new Error( + `[network-vpc-endpoints-stack] Create Route 53 Resolver endpoint: subnet not found in VPC ${vpcItem.name}`, + ); + } + } + // Create endpoint + if (endpointSubnets.length > 0) { + this.createResolverEndpoint(endpointItem, vpcId, endpointSubnets); + } + } + } + } + } + } + + Logger.info('[network-vpc-endpoints-stack] Completed stack synthesis'); + } + + /** + * Create a Network Firewall in the specified VPC and subnets. + * + * @param firewallItem + * @param vpcId + * @param subnets + * @param owningAccountId + * @returns + */ + private createNetworkFirewall( + firewallItem: NfwFirewallConfig, + vpcId: string, + subnets: string[], + owningAccountId?: string, + ): NetworkFirewall { + // Get firewall policy ARN + let policyArn: string; + + if (!owningAccountId) { + policyArn = cdk.aws_ssm.StringParameter.valueForStringParameter( + this, + `/accelerator/network/networkFirewall/policies/${firewallItem.firewallPolicy}/arn`, + ); + } else { + policyArn = this.getResourceShare( + `${firewallItem.firewallPolicy}_NetworkFirewallPolicyShare`, + 'network-firewall:FirewallPolicy', + owningAccountId, + ).resourceShareItemArn; + } + + Logger.info(`[network-vpc-endpoints-stack] Add Network Firewall ${firewallItem.name} to VPC ${firewallItem.vpc}`); + const nfw = new NetworkFirewall(this, pascalCase(`${firewallItem.vpc}${firewallItem.name}NetworkFirewall`), { + firewallPolicyArn: policyArn, + name: firewallItem.name, + description: firewallItem.description, + subnets: subnets, + vpcId: vpcId, + deleteProtection: firewallItem.deleteProtection, + firewallPolicyChangeProtection: firewallItem.firewallPolicyChangeProtection, + subnetChangeProtection: firewallItem.subnetChangeProtection, + tags: firewallItem.tags ?? [], + }); + // Create SSM parameters + new cdk.aws_ssm.StringParameter( + this, + pascalCase(`SsmParam${pascalCase(firewallItem.vpc) + pascalCase(firewallItem.name)}FirewallArn`), + { + parameterName: `/accelerator/network/vpc/${firewallItem.vpc}/networkFirewall/${firewallItem.name}/arn`, + stringValue: nfw.firewallArn, + }, + ); + + // Add logging configurations + let firewallLogBucket: cdk.aws_s3.IBucket | undefined; + const destinationConfigs: cdk.aws_networkfirewall.CfnLoggingConfiguration.LogDestinationConfigProperty[] = []; + for (const logItem of firewallItem.loggingConfiguration ?? []) { + if (logItem.destination === 'cloud-watch-logs') { + // Create log group and log configuration + Logger.info( + `[network-vpc-endpoints-stack] Add CloudWatch ${logItem.type} logs for Network Firewall ${firewallItem.name}`, + ); + const logGroup = new cdk.aws_logs.LogGroup(this, pascalCase(`${firewallItem.name}${logItem.type}LogGroup`), { + encryptionKey: this.acceleratorKey, + retention: this.logRetention, + }); + destinationConfigs.push({ + logDestination: { + logGroup: logGroup.logGroupName, + }, + logDestinationType: 'CloudWatchLogs', + logType: logItem.type, + }); + } + + if (logItem.destination === 's3') { + Logger.info( + `[network-vpc-endpoints-stack] Add S3 ${logItem.type} logs for Network Firewall ${firewallItem.name}`, + ); + + if (!firewallLogBucket) { + firewallLogBucket = cdk.aws_s3.Bucket.fromBucketName( + this, + 'FirewallLogsBucket', + `aws-accelerator-central-logs-${this.accountsConfig.getLogArchiveAccountId()}-${ + this.globalConfig.homeRegion + }`, + ); + } + + destinationConfigs.push({ + logDestination: { + bucketName: firewallLogBucket.bucketName, + }, + logDestinationType: 'S3', + logType: logItem.type, + }); + } + } + + // Add logging configuration + const config = { + logDestinationConfigs: destinationConfigs, + }; + nfw.addLogging(config); + + return nfw; + } + + /** + * Create gateway endpoints for the specified VPC. + * + * @param vpcItem + * @param vpc + * @param routeTableMap + * @param organizationId + */ + private createGatewayEndpoints( + vpcItem: VpcConfig, + vpcId: string, + routeTableMap: Map, + //organizationId?: string, + ): void { + // Create a list of related route tables that will need to be updated with the gateway routes + const s3EndpointRouteTables: string[] = []; + const dynamodbEndpointRouteTables: string[] = []; + for (const routeTableItem of vpcItem.routeTables ?? []) { + const routeTableKey = `${vpcItem.name}_${routeTableItem.name}`; + const routeTableId = routeTableMap.get(routeTableKey); + + if (!routeTableId) { + throw new Error(`[network-vpc-endpoints-stack] Route Table ${routeTableItem.name} not found`); + } + + for (const routeTableEntryItem of routeTableItem.routes ?? []) { + // Route: S3 Gateway Endpoint + if (routeTableEntryItem.target === 's3') { + if (!s3EndpointRouteTables.find(item => item === routeTableId)) { + s3EndpointRouteTables.push(routeTableId); + } + } + + // Route: DynamoDb Gateway Endpoint + if (routeTableEntryItem.target === 'dynamodb') { + if (!dynamodbEndpointRouteTables.find(item => item === routeTableId)) { + dynamodbEndpointRouteTables.push(routeTableId); + } + } + } + } + + // + // Add Gateway Endpoints (AWS Services) + // + for (const gatewayEndpointItem of vpcItem.gatewayEndpoints?.endpoints ?? []) { + Logger.info(`[network-vpc-endpoints-stack] Adding Gateway Endpoint for ${gatewayEndpointItem.service}`); + + if (gatewayEndpointItem.service === 's3') { + new VpcEndpoint(this, pascalCase(`${vpcItem.name}Vpc`) + pascalCase(gatewayEndpointItem.service), { + vpcId, + vpcEndpointType: cdk.aws_ec2.VpcEndpointType.GATEWAY, + service: gatewayEndpointItem.service, + routeTables: s3EndpointRouteTables, + policyDocument: this.createVpcEndpointPolicy(vpcItem, gatewayEndpointItem, true), + }); + } + if (gatewayEndpointItem.service === 'dynamodb') { + new VpcEndpoint(this, pascalCase(`${vpcItem.name}Vpc`) + pascalCase(gatewayEndpointItem.service), { + vpcId, + vpcEndpointType: cdk.aws_ec2.VpcEndpointType.GATEWAY, + service: gatewayEndpointItem.service, + routeTables: dynamodbEndpointRouteTables, + policyDocument: this.createVpcEndpointPolicy(vpcItem, gatewayEndpointItem, true), + }); + } + } + } + + /** + * Create interface endpoints for the specified VPC. + * + * @param vpcItem + * @param vpc + * @param subnetMap + */ + private createInterfaceEndpoints( + vpcItem: VpcConfig, + vpcId: string, + subnetMap: Map, + //organizationId?: string, + ): void { + // + // Add Interface Endpoints (AWS Services) + // + // Create list of subnet IDs for each interface endpoint + const subnets: string[] = []; + for (const subnetItem of vpcItem.interfaceEndpoints?.subnets ?? []) { + const subnetKey = `${vpcItem.name}_${subnetItem}`; + const subnet = subnetMap.get(subnetKey); + if (subnet) { + subnets.push(subnet); + } else { + throw new Error( + `[network-vpc-endpoints-stack] Attempting to add interface endpoints to subnet that does not exist (${subnetItem})`, + ); + } + } + + // Create the interface endpoint + const securityGroupMap = new Map(); + let endpointSg: SecurityGroup | undefined; + let port: number; + let trafficType: string; + for (const endpointItem of vpcItem.interfaceEndpoints?.endpoints ?? []) { + Logger.info(`[network-vpc-endpoints-stack] Adding Interface Endpoint for ${endpointItem.service}`); + + if (endpointItem.service !== 'cassandra') { + endpointSg = securityGroupMap.get('https'); + port = 443; + trafficType = 'https'; + } else { + endpointSg = securityGroupMap.get('cassandra'); + port = 9142; + trafficType = 'cassandra'; + } + + // Create Security Group if it doesn't exist + if (!endpointSg) { + const ingressRules: SecurityGroupIngressRuleProps[] = []; + const egressRules: SecurityGroupEgressRuleProps[] = []; + let includeNagSuppression = false; + + // Add ingress and egress CIDRs + for (const ingressCidr of vpcItem.interfaceEndpoints?.allowedCidrs || ['0.0.0.0/0']) { + Logger.info( + `[network-vpc-endpoints-stack] Interface endpoints: adding ingress cidr ${ingressCidr} TCP:${port}`, + ); + ingressRules.push({ + ipProtocol: cdk.aws_ec2.Protocol.TCP, + fromPort: port, + toPort: port, + cidrIp: ingressCidr, + }); + + // AwsSolutions-EC23: The Security Group allows for 0.0.0.0/0 or ::/0 inbound access. + // rule suppression with evidence for this permission. + if (ingressCidr === '0.0.0.0/0') { + includeNagSuppression = true; + } + } + + // Adding Egress '127.0.0.1/32' to avoid default Egress rule + egressRules.push({ + ipProtocol: cdk.aws_ec2.Protocol.ALL, + cidrIp: '127.0.0.1/32', + }); + + // Create Security Group + Logger.info( + `[network-vpc-endpoints-stack] Adding Security Group to VPC ${vpcItem.name} for interface endpoints -- ${trafficType} traffic`, + ); + const securityGroup = new SecurityGroup(this, pascalCase(`${vpcItem.name}Vpc${trafficType}EpSecurityGroup`), { + securityGroupName: `interface_ep_${trafficType}_sg`, + securityGroupEgress: egressRules, + securityGroupIngress: ingressRules, + description: `Security group for interface endpoints -- ${trafficType} traffic`, + vpcId, + }); + endpointSg = securityGroup; + securityGroupMap.set(trafficType, securityGroup); + + // AwsSolutions-EC23: The Security Group allows for 0.0.0.0/0 or ::/0 inbound access. + // rule suppression with evidence for this permission. + if (includeNagSuppression) { + NagSuppressions.addResourceSuppressionsByPath( + this, + `${this.stackName}/${pascalCase(vpcItem.name)}Vpc${trafficType}EpSecurityGroup`, + [ + { + id: 'AwsSolutions-EC23', + reason: 'Allowed access for interface endpoints', + }, + ], + ); + } + } + + // Create the interface endpoint + const endpoint = new VpcEndpoint(this, `${pascalCase(vpcItem.name)}Vpc${pascalCase(endpointItem.service)}Ep`, { + vpcId, + vpcEndpointType: cdk.aws_ec2.VpcEndpointType.INTERFACE, + service: endpointItem.service, + subnets, + securityGroups: [endpointSg], + privateDnsEnabled: false, + policyDocument: this.createVpcEndpointPolicy(vpcItem, endpointItem), + }); + new cdk.aws_ssm.StringParameter(this, pascalCase(`SsmParam${vpcItem.name}${endpointItem.service}Dns`), { + parameterName: `/accelerator/network/vpc/${vpcItem.name}/endpoints/${endpointItem.service}/dns`, + stringValue: endpoint.dnsName!, + }); + new cdk.aws_ssm.StringParameter(this, pascalCase(`SsmParam${vpcItem.name}${endpointItem.service}Phz`), { + parameterName: `/accelerator/network/vpc/${vpcItem.name}/endpoints/${endpointItem.service}/hostedZoneId`, + stringValue: endpoint.hostedZoneId!, + }); + } + } + + // + // Create Route 53 Resolver endpoints + // + private createResolverEndpoint(endpointItem: ResolverEndpointConfig, vpcId: string, subnets: string[]): void { + // Validate there are no rules associated with an inbound endpoint + if (endpointItem.type === 'INBOUND' && endpointItem.rules) { + throw new Error('[network-vpc-endpoints-stack] Route 53 Resolver inbound endpoints cannot have rules.'); + } + + // Begin creation of Route 53 resolver endpoint + Logger.info( + `[network-vpc-endpoints-stack] Add Route 53 Resolver ${endpointItem.type} endpoint ${endpointItem.name}`, + ); + const ingressRules: SecurityGroupIngressRuleProps[] = []; + const egressRules: SecurityGroupEgressRuleProps[] = []; + let includeNagSuppression = false; + + if (endpointItem.type === 'INBOUND') { + for (const ingressCidr of endpointItem.allowedCidrs || ['0.0.0.0/0']) { + const port = 53; + + Logger.info(`[network-vpc-endpoints-stack] Route 53 resolver: adding ingress cidr ${ingressCidr} TCP:${port}`); + ingressRules.push({ + ipProtocol: cdk.aws_ec2.Protocol.TCP, + fromPort: port, + toPort: port, + cidrIp: ingressCidr, + }); + + Logger.info(`[network-vpc-endpoints-stack] Route 53 resolver: adding ingress cidr ${ingressCidr} UDP:${port}`); + ingressRules.push({ + ipProtocol: cdk.aws_ec2.Protocol.UDP, + fromPort: port, + toPort: port, + cidrIp: ingressCidr, + }); + + if (ingressCidr === '0.0.0.0/0') { + // AwsSolutions-EC23: The Security Group allows for 0.0.0.0/0 or ::/0 inbound access. + // rule suppression with evidence for this permission. + includeNagSuppression = true; + } + } + + // Adding Egress '127.0.0.1/32' to avoid default Egress rule + egressRules.push({ + ipProtocol: cdk.aws_ec2.Protocol.ALL, + cidrIp: '127.0.0.1/32', + }); + } else { + // Check if non-standard ports exist in rules + const portMap = new Map(); + for (const ruleItem of endpointItem.rules ?? []) { + for (const targetItem of ruleItem.targetIps ?? []) { + if (targetItem.port) { + portMap.set(targetItem.ip, targetItem.port); + } + } + } + + for (const egressCidr of endpointItem.allowedCidrs || ['0.0.0.0/0']) { + let port = 53; + const nonStandardPort = portMap.get(egressCidr.split('/')[0]); //Split at the prefix to match target IP + + // Check if mapping includes non-standard port + if (nonStandardPort) { + port = +nonStandardPort; + } + + Logger.info(`[network-vpc-endpoints-stack] Route 53 resolver: adding egress cidr ${egressCidr} TCP:${port}`); + egressRules.push({ + ipProtocol: cdk.aws_ec2.Protocol.TCP, + fromPort: port, + toPort: port, + cidrIp: egressCidr, + }); + + Logger.info(`[network-vpc-endpoints-stack] Route 53 resolver: adding egress cidr ${egressCidr} UDP:${port}`); + egressRules.push({ + ipProtocol: cdk.aws_ec2.Protocol.UDP, + fromPort: port, + toPort: port, + cidrIp: egressCidr, + }); + } + } + + // Create security group + Logger.info( + `[network-vpc-endpoints-stack] Adding Security Group for Route 53 Resolver endpoint ${endpointItem.name}`, + ); + const securityGroup = new SecurityGroup(this, pascalCase(`${endpointItem.name}EpSecurityGroup`), { + securityGroupName: `ep_${endpointItem.name}_sg`, + securityGroupEgress: egressRules, + securityGroupIngress: ingressRules, + description: `AWS Route 53 Resolver endpoint - ${endpointItem.name}`, + vpcId, + }); + + // AwsSolutions-EC23: The Security Group allows for 0.0.0.0/0 or ::/0 inbound access. + // rule suppression with evidence for this permission. + if (includeNagSuppression) { + NagSuppressions.addResourceSuppressionsByPath( + this, + `${this.stackName}/${pascalCase(endpointItem.name)}EpSecurityGroup`, + [ + { + id: 'AwsSolutions-EC23', + reason: 'Allowed access for interface endpoints', + }, + ], + ); + } + + // Create resolver endpoint + const endpoint = new ResolverEndpoint(this, `${pascalCase(endpointItem.name)}ResolverEndpoint`, { + direction: endpointItem.type, + ipAddresses: subnets, + name: endpointItem.name, + securityGroupIds: [securityGroup.securityGroupId], + tags: endpointItem.tags ?? [], + }); + new cdk.aws_ssm.StringParameter(this, pascalCase(`SsmParam${endpointItem.name}ResolverEndpoint`), { + parameterName: `/accelerator/network/route53Resolver/endpoints/${endpointItem.name}/id`, + stringValue: endpoint.endpointId, + }); + } + + /** + * Creates a cdk.aws_iam.PolicyDocument for the given endpoint. + * @param vpcItem + * @param endpointItem + * @param isGatewayEndpoint + * @returns + */ + private createVpcEndpointPolicy( + vpcItem: VpcConfig, + endpointItem: GatewayEndpointServiceConfig | InterfaceEndpointServiceConfig, + isGatewayEndpoint?: boolean, + ): cdk.aws_iam.PolicyDocument | undefined { + // See https://docs.aws.amazon.com/vpc/latest/privatelink/integrated-services-vpce-list.html + // for the services that integrates with AWS PrivateLink, but does not support VPC endpoint policies + const policiesUnsupported = [ + 'appmesh-envoy-management', + 'appstream.api', + 'appstream.streaming', + 'cloudtrail', + 'codeguru-profiler', + 'codeguru-reviewer', + 'codepipeline', + 'datasync', + 'ebs', + 'elastic-inference.runtime', + 'iot.data', + 'iotwireless.api', + 'lorawan.cups', + 'lorawan.lns', + 'iotsitewise.api', + 'iotsitewise.data', + 'macie2', + 'aps', + 'aps-workspaces', + 'awsconnector', + 'sms', + 'sms-fips', + 'email-smtp', + 'storagegateway', + 'transfer', + 'transfer.server', + ]; + + if (policiesUnsupported.includes(endpointItem.service)) { + return undefined; + } + + // Identify if custom policy is specified, create custom or default policy + let policyName: string | undefined; + let policyDocument: cdk.aws_iam.PolicyDocument | undefined = undefined; + if (endpointItem.policy) { + Logger.info(`[network-vpc-endpoints-stack] Add custom endpoint policy for ${endpointItem.service}`); + policyName = endpointItem.policy; + } else if (!endpointItem.policy && isGatewayEndpoint) { + Logger.info( + `[network-vpc-endpoints-stack] Add default endpoint policy for gateway endpoint ${endpointItem.service}`, + ); + policyName = vpcItem.gatewayEndpoints?.defaultPolicy; + } else { + Logger.info( + `[network-vpc-endpoints-stack] Add default endpoint policy for interface endpoint ${endpointItem.service}`, + ); + policyName = vpcItem.interfaceEndpoints?.defaultPolicy; + } + + // Find matching endpoint policy item + if (!policyName) { + throw new Error(`[network-vpc-endpoints-stack] Create endpoint policy: unable to set a policy name.`); + } + const policyItem = this.props.networkConfig.endpointPolicies.filter(item => item.name === policyName); + + // Verify there is only one endpoint policy with the same name + if (policyItem.length > 1) { + throw new Error( + `[network-vpc-endpoints-stack] Create endpoint policy: more than one policy with the name ${policyName} is configured.`, + ); + } else if (policyItem.length === 0) { + throw new Error( + `[network-vpc-endpoints-stack] Create endpoint policy: unable to locate policy with the name ${policyName}.`, + ); + } + + // Set location and fetch document + const location = path.join(this.props.configDirPath, policyItem[0].document); + const document = fs.readFileSync(location, 'utf-8'); + + // Set and return policy document + policyDocument = cdk.aws_iam.PolicyDocument.fromJson(JSON.parse(document)); + return policyDocument; + } + + /** + * Get the resource ID from a RAM share. + * + * @param resourceShareName + * @param itemType + * @param owningAccountId + */ + private getResourceShare(resourceShareName: string, itemType: string, owningAccountId: string): IResourceShareItem { + // Generate a logical ID + const resourceName = resourceShareName.split('_')[0]; + const logicalId = `${resourceName}${itemType.split(':')[1]}`; + + // Lookup resource share + const resourceShare = ResourceShare.fromLookup(this, pascalCase(`${logicalId}Share`), { + resourceShareOwner: ResourceShareOwner.OTHER_ACCOUNTS, + resourceShareName: resourceShareName, + owningAccountId, + }); + + // Represents the item shared by RAM + const item = ResourceShareItem.fromLookup(this, pascalCase(`${logicalId}`), { + resourceShare, + resourceShareItemType: itemType, + kmsKey: this.acceleratorKey, + logRetentionInDays: this.logRetention, + }); + return item; + } +} diff --git a/source/packages/@aws-accelerator/accelerator/lib/stacks/network-vpc-stack.ts b/source/packages/@aws-accelerator/accelerator/lib/stacks/network-vpc-stack.ts new file mode 100644 index 000000000..21ec42fac --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/lib/stacks/network-vpc-stack.ts @@ -0,0 +1,1283 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { NagSuppressions } from 'cdk-nag'; +import { pascalCase } from 'change-case'; +import { Construct } from 'constructs'; + +import { + AccountsConfig, + NetworkAclSubnetSelection, + NetworkConfigTypes, + nonEmptyString, + OrganizationConfig, + PrefixListSourceConfig, + SecurityGroupRuleConfig, + SecurityGroupSourceConfig, + SubnetConfig, + SubnetSourceConfig, +} from '@aws-accelerator/config'; +import { + DeleteDefaultVpc, + DhcpOptions, + IResourceShareItem, + KeyLookup, + NatGateway, + NetworkAcl, + PrefixList, + ResourceShare, + ResourceShareItem, + ResourceShareOwner, + RouteTable, + SecurityGroup, + SecurityGroupEgressRuleProps, + SecurityGroupIngressRuleProps, + Subnet, + TransitGatewayAttachment, + Vpc, +} from '@aws-accelerator/constructs'; + +import { Logger } from '../logger'; +import { AcceleratorStack, AcceleratorStackProps } from './accelerator-stack'; +import { KeyStack } from './key-stack'; + +export interface SecurityGroupRuleProps { + ipProtocol: string; + cidrIp?: string; + cidrIpv6?: string; + fromPort?: number; + toPort?: number; + targetSecurityGroup?: SecurityGroup; + targetPrefixList?: PrefixList; + description?: string; +} + +const TCP_PROTOCOLS_PORT: { [key: string]: number } = { + RDP: 3389, + SSH: 22, + HTTP: 80, + HTTPS: 443, + MSSQL: 1433, + 'MYSQL/AURORA': 3306, + REDSHIFT: 5439, + POSTGRESQL: 5432, + 'ORACLE-RDS': 1521, +}; + +type ResourceShareType = SubnetConfig; + +export class NetworkVpcStack extends AcceleratorStack { + private accountsConfig: AccountsConfig; + private orgConfig: OrganizationConfig; + private logRetention: number; + readonly acceleratorKey: cdk.aws_kms.Key; + + constructor(scope: Construct, id: string, props: AcceleratorStackProps) { + super(scope, id, props); + + // Set private properties + this.accountsConfig = props.accountsConfig; + this.orgConfig = props.organizationConfig; + this.logRetention = props.globalConfig.cloudwatchLogRetentionInDays; + + this.acceleratorKey = new KeyLookup(this, 'AcceleratorKeyLookup', { + accountId: props.accountsConfig.getAuditAccountId(), + roleName: KeyStack.CROSS_ACCOUNT_ACCESS_ROLE_NAME, + keyArnParameterName: KeyStack.ACCELERATOR_KEY_ARN_PARAMETER_NAME, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }).getKey(); + + // Get the organization object, used by Data Protection + //const organizationId = props.organizationConfig.enable ? new Organization(this, 'Organization').id : ''; + + // + // Delete Default VPCs + // + if ( + props.networkConfig.defaultVpc?.delete && + !this.isAccountExcluded(props.networkConfig.defaultVpc.excludeAccounts) + ) { + Logger.info('[network-vpc-stack] Add DeleteDefaultVpc'); + new DeleteDefaultVpc(this, 'DeleteDefaultVpc', { + kmsKey: this.acceleratorKey, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }); + } + + // Build map of Transit Gateways. We need to know the transit gateway ids so + // we can create attachments against them. Transit gateways that were + // generated outside this account should have been shared during the + // previous stack phase + const transitGatewayIds = new Map(); + + // Keep track of all the external accounts that will need to be able to list + // the generated transit gateway attachments + const transitGatewayAccountIds: string[] = []; + for (const vpcItem of props.networkConfig.vpcs ?? []) { + const accountId = this.accountsConfig.getAccountId(vpcItem.account); + // Only care about VPCs to be created in the current account and region + if (accountId === cdk.Stack.of(this).account && vpcItem.region == cdk.Stack.of(this).region) { + for (const attachment of vpcItem.transitGatewayAttachments ?? []) { + Logger.info(`[network-vpc-stack] Evaluating Transit Gateway key ${attachment.transitGateway.name}`); + + // Keep looking if already entered + if (transitGatewayIds.has(attachment.transitGateway.name)) { + Logger.info(`[network-vpc-stack] Transit Gateway ${attachment.transitGateway.name} already in dictionary`); + continue; + } + + Logger.info( + `[network-vpc-stack] Transit Gateway key ${attachment.transitGateway.name} is not in map, add resources to look up`, + ); + const owningAccountId = this.accountsConfig.getAccountId(attachment.transitGateway.account); + + // If owning account is this account, transit gateway id can be + // retrieved from ssm parameter store + if (owningAccountId === cdk.Stack.of(this).account) { + const transitGatewayId = cdk.aws_ssm.StringParameter.valueForStringParameter( + this, + `/accelerator/network/transitGateways/${attachment.transitGateway.name}/id`, + ); + + Logger.info( + `[network-vpc-stack] Adding [${attachment.transitGateway.name}]: ${transitGatewayId} to transitGatewayIds Map`, + ); + transitGatewayIds.set(attachment.transitGateway.name, transitGatewayId); + } + // Else, need to get the transit gateway from the resource shares + else { + // Add to transitGatewayAccountIds list so we can create a cross + // account access role to list the created attachments + if (transitGatewayAccountIds.indexOf(owningAccountId) == -1) { + transitGatewayAccountIds.push(owningAccountId); + } + + // Get the resource share related to the transit gateway + const tgwId = this.getResourceShare( + `${attachment.transitGateway.name}_TransitGatewayShare`, + 'ec2:TransitGateway', + owningAccountId, + ).resourceShareItemId; + + Logger.info( + `[network-vpc-stack] Adding [${attachment.transitGateway.name}]: ${tgwId} to transitGatewayIds Map`, + ); + transitGatewayIds.set(attachment.transitGateway.name, tgwId); + } + } + } + } + + // Create cross account access role to read transit gateway attachments if + // there are other accounts in the list + if (transitGatewayAccountIds.length > 0) { + Logger.info(`[network-vpc-stack] Create IAM Cross Account Access Role`); + + const principals: cdk.aws_iam.PrincipalBase[] = []; + transitGatewayAccountIds.forEach(accountId => { + principals.push(new cdk.aws_iam.AccountPrincipal(accountId)); + }); + new cdk.aws_iam.Role(this, 'DescribeTgwAttachRole', { + roleName: `AWSAccelerator-DescribeTgwAttachRole-${cdk.Stack.of(this).region}`, + assumedBy: new cdk.aws_iam.CompositePrincipal(...principals), + inlinePolicies: { + default: new cdk.aws_iam.PolicyDocument({ + statements: [ + new cdk.aws_iam.PolicyStatement({ + effect: cdk.aws_iam.Effect.ALLOW, + actions: ['ec2:DescribeTransitGatewayAttachments'], + resources: ['*'], + }), + ], + }), + }, + }); + + // AwsSolutions-IAM5: The IAM entity contains wildcard permissions and does not have a cdk_nag rule suppression with evidence for those permission. + // rule suppression with evidence for this permission. + NagSuppressions.addResourceSuppressionsByPath(this, `${this.stackName}/DescribeTgwAttachRole/Resource`, [ + { + id: 'AwsSolutions-IAM5', + reason: 'DescribeTgwAttachRole needs access to every describe each transit gateway attachment in the account', + }, + ]); + } + + // Get the CentralLogsBucket, if needed to send vpc flow logs to + let centralLogsBucketArn: string | undefined = undefined; + if (props.networkConfig.vpcFlowLogs.destinations.includes('s3')) { + Logger.info(`[network-vpc-stack] S3 destination for VPC flow log detected, obtain the CentralLogsBucket`); + + const centralLogsBucket = cdk.aws_s3.Bucket.fromBucketName( + this, + 'CentralLogsBucket', + `aws-accelerator-central-logs-${this.accountsConfig.getLogArchiveAccountId()}-${props.globalConfig.homeRegion}`, + ); + centralLogsBucketArn = centralLogsBucket.bucketArn; + } + + // + // Check to see if useCentralEndpoints is enabled for any other VPC within + // this account and region. If so, we will need to create a cross account + // access role (if we're in a different account) + // + let useCentralEndpoints = false; + for (const vpcItem of props.networkConfig.vpcs ?? []) { + const accountId = this.accountsConfig.getAccountId(vpcItem.account); + if (accountId === cdk.Stack.of(this).account && vpcItem.region == cdk.Stack.of(this).region) { + if (vpcItem.useCentralEndpoints) { + useCentralEndpoints = true; + } + } + } + + // + // Find and validate the central endpoints vpc + // + let centralEndpointVpc = undefined; + if (useCentralEndpoints) { + Logger.info('[network-vpc-stack] VPC found in this account with useCentralEndpoints set to true'); + + // Find the central endpoints vpc (should only be one) + const centralEndpointVpcs = props.networkConfig.vpcs.filter( + item => item.interfaceEndpoints?.central && item.region === cdk.Stack.of(this).region, + ); + if (centralEndpointVpcs.length === 0) { + throw new Error('useCentralEndpoints set to true, but no central endpoint vpc detected, should be exactly one'); + } + if (centralEndpointVpcs.length > 1) { + throw new Error( + 'useCentralEndpoints set to true, but multiple central endpoint vpcs detected, should only be one', + ); + } + centralEndpointVpc = centralEndpointVpcs[0]; + } + + // + // Using central endpoints, create a cross account access role, if in + // external account + // + if (centralEndpointVpc) { + const centralEndpointVpcAccountId = this.accountsConfig.getAccountId(centralEndpointVpc.account); + if (centralEndpointVpcAccountId !== cdk.Stack.of(this).account) { + Logger.info( + '[network-vpc-stack] Central Endpoints VPC is in an external account, create a role to enable central endpoints', + ); + new cdk.aws_iam.Role(this, 'EnableCentralEndpointsRole', { + roleName: `AWSAccelerator-EnableCentralEndpointsRole-${cdk.Stack.of(this).region}`, + assumedBy: new cdk.aws_iam.AccountPrincipal(centralEndpointVpcAccountId), + inlinePolicies: { + default: new cdk.aws_iam.PolicyDocument({ + statements: [ + new cdk.aws_iam.PolicyStatement({ + effect: cdk.aws_iam.Effect.ALLOW, + actions: ['ec2:DescribeVpcs', 'route53:AssociateVPCWithHostedZone'], + resources: ['*'], + }), + ], + }), + }, + }); + + // AwsSolutions-IAM5: The IAM entity contains wildcard permissions and does not have a cdk_nag rule suppression with evidence for those permission. + // rule suppression with evidence for this permission. + NagSuppressions.addResourceSuppressionsByPath( + this, + `${this.stackName}/EnableCentralEndpointsRole/Resource/Resource`, + [ + { + id: 'AwsSolutions-IAM5', + reason: 'EnableCentralEndpointsRole needs access to every describe every VPC in the account ', + }, + ], + ); + } + } + + // + // Loop through VPC peering entries. Determine if accepter VPC is in external account. + // Add VPC peering role to external account IDs if necessary + // + const vpcPeeringAccountIds: string[] = []; + for (const peering of props.networkConfig.vpcPeering ?? []) { + // Check to ensure only two VPCs are defined + if (peering.vpcs.length > 2) { + throw new Error(`[network-vpc-stack] VPC peering connection ${peering.name} has more than two VPCs defined`); + } + + // Get requester and accepter VPC configurations + const requesterVpc = props.networkConfig.vpcs.filter(item => item.name === peering.vpcs[0]); + const accepterVpc = props.networkConfig.vpcs.filter(item => item.name === peering.vpcs[1]); + + if (requesterVpc.length === 1 && accepterVpc.length === 1) { + const requesterAccountId = props.accountsConfig.getAccountId(requesterVpc[0].account); + const accepterAccountId = props.accountsConfig.getAccountId(accepterVpc[0].account); + + // Check for different account peering -- only add IAM role to accepter account + if (cdk.Stack.of(this).account === accepterAccountId && cdk.Stack.of(this).region === accepterVpc[0].region) { + if (requesterAccountId !== accepterAccountId && !vpcPeeringAccountIds.includes(requesterAccountId)) { + vpcPeeringAccountIds.push(requesterAccountId); + } + } + } else if (requesterVpc.length === 0) { + throw new Error(`[network-vpc-stack] Requester VPC ${peering.vpcs[0]} is undefined`); + } else if (accepterVpc.length === 0) { + throw new Error(`[network-vpc-stack] Accepter VPC ${peering.vpcs[1]} is undefined`); + } else { + throw new Error(`[network-vpc-stack] network-config.yaml cannot contain VPCs with the same name`); + } + } + + // + // Create VPC peering role + // + if (vpcPeeringAccountIds.length > 0) { + Logger.info(`[network-vpc-stack] Create cross-account IAM role for VPC peering`); + + const principals: cdk.aws_iam.PrincipalBase[] = []; + vpcPeeringAccountIds.forEach(accountId => { + principals.push(new cdk.aws_iam.AccountPrincipal(accountId)); + }); + new cdk.aws_iam.Role(this, 'VpcPeeringRole', { + roleName: `AWSAccelerator-VpcPeeringRole-${cdk.Stack.of(this).region}`, + assumedBy: new cdk.aws_iam.CompositePrincipal(...principals), + inlinePolicies: { + default: new cdk.aws_iam.PolicyDocument({ + statements: [ + new cdk.aws_iam.PolicyStatement({ + effect: cdk.aws_iam.Effect.ALLOW, + actions: [ + 'ec2:AcceptVpcPeeringConnection', + 'ec2:CreateVpcPeeringConnection', + 'ec2:DeleteVpcPeeringConnection', + ], + resources: [ + `arn:${cdk.Aws.PARTITION}:ec2:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:vpc/*`, + `arn:${cdk.Aws.PARTITION}:ec2:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:vpc-peering-connection/*`, + ], + }), + new cdk.aws_iam.PolicyStatement({ + effect: cdk.aws_iam.Effect.ALLOW, + actions: ['ssm:DeleteParameter', 'ssm:PutParameter'], + resources: [ + `arn:${cdk.Aws.PARTITION}:ssm:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:parameter/accelerator/network/vpcPeering/*`, + ], + }), + new cdk.aws_iam.PolicyStatement({ + effect: cdk.aws_iam.Effect.ALLOW, + actions: ['ssm:GetParameter'], + resources: [ + `arn:${cdk.Aws.PARTITION}:ssm:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:parameter/accelerator/network/vpc/*`, + ], + }), + ], + }), + }, + }); + } + + // + // Create DHCP options + // + // Create map to store DHCP options + const dhcpOptionsIds = new Map(); + + for (const dhcpItem of props.networkConfig.dhcpOptions ?? []) { + // Check if the set belongs in this account/region + const accountIds = dhcpItem.accounts.map(item => { + return this.accountsConfig.getAccountId(item); + }); + const regions = dhcpItem.regions.map(item => { + return item.toString(); + }); + + if (accountIds.includes(cdk.Stack.of(this).account) && regions.includes(cdk.Stack.of(this).region)) { + Logger.info(`[network-vpc-stack] Adding DHCP options set ${dhcpItem.name}`); + + const optionSet = new DhcpOptions(this, pascalCase(`${dhcpItem.name}DhcpOpts`), { + name: dhcpItem.name, + domainName: dhcpItem.domainName, + domainNameServers: dhcpItem.domainNameServers, + netbiosNameServers: dhcpItem.netbiosNameServers, + netbiosNodeType: dhcpItem.netbiosNodeType, + ntpServers: dhcpItem.ntpServers, + tags: dhcpItem.tags ?? [], //Default passing an empty array for name tag + }); + dhcpOptionsIds.set(optionSet.name, optionSet.dhcpOptionsId); + } + } + + // + // Create Prefix Lists + // + // Create map to store Prefix List + const prefixListMap = new Map(); + + for (const prefixListItem of props.networkConfig.prefixLists ?? []) { + // Check if the set belongs in this account/region + const accountIds = prefixListItem.accounts.map(item => { + return this.accountsConfig.getAccountId(item); + }); + const regions = prefixListItem.regions.map(item => { + return item.toString(); + }); + + if (accountIds.includes(cdk.Stack.of(this).account) && regions.includes(cdk.Stack.of(this).region)) { + Logger.info(`[network-vpc-stack] Adding Prefix List ${prefixListItem.name}`); + + const prefixList = new PrefixList(this, pascalCase(`${prefixListItem.name}PrefixList`), { + name: prefixListItem.name, + addressFamily: prefixListItem.addressFamily, + maxEntries: prefixListItem.maxEntries, + entries: prefixListItem.entries, + tags: prefixListItem.tags ?? [], + }); + + prefixListMap.set(prefixListItem.name, prefixList); + + new cdk.aws_ssm.StringParameter(this, pascalCase(`SsmParam${pascalCase(prefixListItem.name)}PrefixList`), { + parameterName: `/accelerator/network/prefixList/${prefixListItem.name}/id`, + stringValue: prefixList.prefixListId, + }); + } + } + + // + // Evaluate VPC entries + // + for (const vpcItem of props.networkConfig.vpcs ?? []) { + const accountId = this.accountsConfig.getAccountId(vpcItem.account); + if (accountId === cdk.Stack.of(this).account && vpcItem.region == cdk.Stack.of(this).region) { + Logger.info(`[network-vpc-stack] Adding VPC ${vpcItem.name}`); + + // + // Create the VPC + // + const vpc = new Vpc(this, pascalCase(`${vpcItem.name}Vpc`), { + name: vpcItem.name, + ipv4CidrBlock: vpcItem.cidrs[0], + internetGateway: vpcItem.internetGateway, + dhcpOptions: dhcpOptionsIds.get(vpcItem.dhcpOptions ?? ''), + enableDnsHostnames: vpcItem.enableDnsHostnames ?? true, + enableDnsSupport: vpcItem.enableDnsSupport ?? true, + instanceTenancy: vpcItem.instanceTenancy ?? 'default', + tags: vpcItem.tags, + }); + new cdk.aws_ssm.StringParameter(this, pascalCase(`SsmParam${pascalCase(vpcItem.name)}VpcId`), { + parameterName: `/accelerator/network/vpc/${vpcItem.name}/id`, + stringValue: vpc.vpcId, + }); + + // + // Tag the VPC if central endpoints are enabled. These tags are used to + // identify which VPCs in a target account to create private hosted zone + // associations for. + // + if (vpcItem.useCentralEndpoints) { + if (centralEndpointVpc === undefined) { + throw new Error('Attempting to use central endpoints with no Central Endpoints defined'); + } + cdk.Tags.of(vpc).add('accelerator:use-central-endpoints', 'true'); + cdk.Tags.of(vpc).add( + 'accelerator:central-endpoints-account-id', + this.accountsConfig.getAccountId(centralEndpointVpc.account), + ); + } + + // + // Create VPC Flow Log + // + let logFormat: string | undefined = undefined; + if (!props.networkConfig.vpcFlowLogs.defaultFormat) { + logFormat = props.networkConfig.vpcFlowLogs.customFields.map(c => `$\{${c}}`).join(' '); + } + vpc.addFlowLogs({ + destinations: props.networkConfig.vpcFlowLogs.destinations, + trafficType: props.networkConfig.vpcFlowLogs.trafficType, + maxAggregationInterval: props.networkConfig.vpcFlowLogs.maxAggregationInterval, + logFormat, + logRetentionInDays: this.logRetention, + encryptionKey: this.acceleratorKey, + bucketArn: centralLogsBucketArn, + }); + + // + // Create Route Tables + // + const routeTableMap = new Map(); + for (const routeTableItem of vpcItem.routeTables ?? []) { + const routeTable = new RouteTable( + this, + pascalCase(`${vpcItem.name}Vpc`) + pascalCase(`${routeTableItem.name}RouteTable`), + { + name: routeTableItem.name, + vpc, + tags: routeTableItem.tags, + }, + ); + routeTableMap.set(routeTableItem.name, routeTable); + new cdk.aws_ssm.StringParameter( + this, + pascalCase(`SsmParam${pascalCase(vpcItem.name) + pascalCase(routeTableItem.name)}RouteTableId`), + { + parameterName: `/accelerator/network/vpc/${vpcItem.name}/routeTable/${routeTableItem.name}/id`, + stringValue: routeTable.routeTableId, + }, + ); + } + + // + // Create Subnets + // + const subnetMap = new Map(); + for (const subnetItem of vpcItem.subnets ?? []) { + Logger.info(`[network-vpc-stack] Adding subnet ${subnetItem.name}`); + + const routeTable = routeTableMap.get(subnetItem.routeTable); + if (routeTable === undefined) { + throw new Error(`Route table ${subnetItem.routeTable} not defined`); + } + + const subnet = new Subnet(this, pascalCase(`${vpcItem.name}Vpc`) + pascalCase(`${subnetItem.name}Subnet`), { + name: subnetItem.name, + availabilityZone: `${cdk.Stack.of(this).region}${subnetItem.availabilityZone}`, + ipv4CidrBlock: subnetItem.ipv4CidrBlock, + mapPublicIpOnLaunch: subnetItem.mapPublicIpOnLaunch, + routeTable, + vpc, + tags: subnetItem.tags, + }); + subnetMap.set(subnetItem.name, subnet); + new cdk.aws_ssm.StringParameter( + this, + pascalCase(`SsmParam${pascalCase(vpcItem.name) + pascalCase(subnetItem.name)}SubnetId`), + { + parameterName: `/accelerator/network/vpc/${vpcItem.name}/subnet/${subnetItem.name}/id`, + stringValue: subnet.subnetId, + }, + ); + + if (subnetItem.shareTargets) { + Logger.info(`[network-vpc-stack] Share subnet`); + this.addResourceShare(subnetItem, `${subnetItem.name}_SubnetShare`, [subnet.subnetArn]); + } + } + + // + // Create NAT Gateways + // + const natGatewayMap = new Map(); + for (const natGatewayItem of vpcItem.natGateways ?? []) { + Logger.info(`[network-vpc-stack] Adding NAT Gateway ${natGatewayItem.name}`); + + const subnet = subnetMap.get(natGatewayItem.subnet); + if (subnet === undefined) { + throw new Error(`Subnet ${natGatewayItem.subnet} not defined`); + } + + const natGateway = new NatGateway( + this, + pascalCase(`${vpcItem.name}Vpc`) + pascalCase(`${natGatewayItem.name}NatGateway`), + { + name: natGatewayItem.name, + subnet, + tags: natGatewayItem.tags, + }, + ); + natGatewayMap.set(natGatewayItem.name, natGateway); + new cdk.aws_ssm.StringParameter( + this, + pascalCase(`SsmParam${pascalCase(vpcItem.name) + pascalCase(natGatewayItem.name)}NatGatewayId`), + { + parameterName: `/accelerator/network/vpc/${vpcItem.name}/natGateway/${natGatewayItem.name}/id`, + stringValue: natGateway.natGatewayId, + }, + ); + } + + // + // Create Transit Gateway Attachments + // + const transitGatewayAttachments = new Map(); + for (const tgwAttachmentItem of vpcItem.transitGatewayAttachments ?? []) { + Logger.info( + `[network-vpc-stack] Adding Transit Gateway Attachment for ${tgwAttachmentItem.transitGateway.name}`, + ); + + const transitGatewayId = transitGatewayIds.get(tgwAttachmentItem.transitGateway.name); + if (transitGatewayId === undefined) { + throw new Error(`Transit Gateway ${tgwAttachmentItem.transitGateway.name} not found`); + } + + const subnetIds: string[] = []; + for (const subnetItem of tgwAttachmentItem.subnets ?? []) { + const subnet = subnetMap.get(subnetItem); + if (subnet === undefined) { + throw new Error(`Subnet ${subnetItem} not defined`); + } + subnetIds.push(subnet.subnetId); + } + + const attachment = new TransitGatewayAttachment( + this, + pascalCase(`${tgwAttachmentItem.name}VpcTransitGatewayAttachment`), + { + name: tgwAttachmentItem.name, + partition: this.props.partition, + transitGatewayId, + subnetIds, + vpcId: vpc.vpcId, + options: tgwAttachmentItem.options, + tags: tgwAttachmentItem.tags, + }, + ); + transitGatewayAttachments.set(tgwAttachmentItem.transitGateway.name, attachment); + new cdk.aws_ssm.StringParameter( + this, + pascalCase( + `SsmParam${pascalCase(vpcItem.name) + pascalCase(tgwAttachmentItem.name)}TransitGatewayAttachmentId`, + ), + { + parameterName: `/accelerator/network/vpc/${vpcItem.name}/transitGatewayAttachment/${tgwAttachmentItem.name}/id`, + stringValue: attachment.transitGatewayAttachmentId, + }, + ); + } + + // + // Create Route Table Entries. + // + for (const routeTableItem of vpcItem.routeTables ?? []) { + const routeTable = routeTableMap.get(routeTableItem.name); + + if (routeTable === undefined) { + throw new Error(`Route Table ${routeTableItem.name} not found`); + } + + for (const routeTableEntryItem of routeTableItem.routes ?? []) { + const id = + pascalCase(`${vpcItem.name}Vpc`) + + pascalCase(`${routeTableItem.name}RouteTable`) + + pascalCase(routeTableEntryItem.name); + + // Route: Transit Gateway + if (routeTableEntryItem.type === 'transitGateway') { + Logger.info(`[network-vpc-stack] Adding Transit Gateway Route Table Entry ${routeTableEntryItem.name}`); + + const transitGatewayId = transitGatewayIds.get(routeTableEntryItem.target); + if (transitGatewayId === undefined) { + throw new Error(`Transit Gateway ${routeTableEntryItem.target} not found`); + } + + const transitGatewayAttachment = transitGatewayAttachments.get(routeTableEntryItem.target); + if (transitGatewayAttachment === undefined) { + throw new Error(`Transit Gateway Attachment ${routeTableEntryItem.target} not found`); + } + + routeTable.addTransitGatewayRoute( + id, + routeTableEntryItem.destination, + transitGatewayId, + // TODO: Implement correct dependency relationships without need for escape hatch + transitGatewayAttachment.node.defaultChild as cdk.aws_ec2.CfnTransitGatewayAttachment, + ); + } + + // Route: NAT Gateway + if (routeTableEntryItem.type === 'natGateway') { + Logger.info(`[network-vpc-stack] Adding NAT Gateway Route Table Entry ${routeTableEntryItem.name}`); + + const natGateway = natGatewayMap.get(routeTableEntryItem.target); + if (natGateway === undefined) { + throw new Error(`NAT Gateway ${routeTableEntryItem.target} not found`); + } + + routeTable.addNatGatewayRoute(id, routeTableEntryItem.destination, natGateway.natGatewayId); + } + + // Route: Internet Gateway + if (routeTableEntryItem.type === 'internetGateway') { + Logger.info(`[network-vpc-stack] Adding Internet Gateway Route Table Entry ${routeTableEntryItem.name}`); + routeTable.addInternetGatewayRoute(id, routeTableEntryItem.destination); + } + } + } + + // + // Add Security Groups + // + const securityGroupMap = new Map(); + + for (const securityGroupItem of vpcItem.securityGroups ?? []) { + const processedIngressRules: SecurityGroupIngressRuleProps[] = []; + const processedEgressRules: SecurityGroupEgressRuleProps[] = []; + + Logger.info(`[network-vpc-stack] Adding rules to ${securityGroupItem.name}`); + + // Add ingress rules + for (const [ruleId, ingressRuleItem] of securityGroupItem.inboundRules.entries() ?? []) { + Logger.info(`[network-vpc-stack] Adding ingress rule ${ruleId} to ${securityGroupItem.name}`); + + const ingressRules: SecurityGroupRuleProps[] = this.processSecurityGroupRules( + ingressRuleItem, + prefixListMap, + ); + + Logger.info(`[network-vpc-stack] Adding ${ingressRules.length} ingress rules`); + + for (const ingressRule of ingressRules) { + if (ingressRule.targetPrefixList) { + processedIngressRules.push({ + description: ingressRule.description, + fromPort: ingressRule.fromPort, + ipProtocol: ingressRule.ipProtocol, + sourcePrefixListId: ingressRule.targetPrefixList.prefixListId, + toPort: ingressRule.toPort, + }); + } else { + processedIngressRules.push({ ...ingressRule }); + } + } + } + + // Add egress rules + for (const [ruleId, egressRuleItem] of securityGroupItem.outboundRules.entries() ?? []) { + Logger.info(`[network-vpc-stack] Adding egress rule ${ruleId} to ${securityGroupItem.name}`); + + const egressRules: SecurityGroupRuleProps[] = this.processSecurityGroupRules(egressRuleItem, prefixListMap); + + Logger.info(`[network-vpc-stack] Adding ${egressRules.length} egress rules`); + + for (const egressRule of egressRules) { + if (egressRule.targetPrefixList) { + processedEgressRules.push({ + description: egressRule.description, + destinationPrefixListId: egressRule.targetPrefixList.prefixListId, + fromPort: egressRule.fromPort, + ipProtocol: egressRule.ipProtocol, + toPort: egressRule.toPort, + }); + } else { + processedEgressRules.push({ ...egressRule }); + } + } + } + + // Create security group + Logger.info(`[network-vpc-stack] Adding Security Group ${securityGroupItem.name}`); + const securityGroup = new SecurityGroup( + this, + pascalCase(`${vpcItem.name}Vpc`) + pascalCase(`${securityGroupItem.name}Sg`), + { + securityGroupName: securityGroupItem.name, + securityGroupEgress: processedEgressRules, + securityGroupIngress: processedIngressRules, + description: securityGroupItem.description, + vpc, + tags: securityGroupItem.tags, + }, + ); + securityGroupMap.set(securityGroupItem.name, securityGroup); + + new cdk.aws_ssm.StringParameter( + this, + pascalCase(`SsmParam${pascalCase(vpcItem.name) + pascalCase(securityGroupItem.name)}SecurityGroup`), + { + parameterName: `/accelerator/network/vpc/${vpcItem.name}/securityGroup/${securityGroupItem.name}/id`, + stringValue: securityGroup.securityGroupId, + }, + ); + } + + // Add security group references + for (const securityGroupItem of vpcItem.securityGroups ?? []) { + for (const [ruleId, ingressRuleItem] of securityGroupItem.inboundRules.entries() ?? []) { + // Check if rule sources include a security group reference + let includesSecurityGroupSource = false; + for (const source of ingressRuleItem.sources) { + if (NetworkConfigTypes.securityGroupSourceConfig.is(source)) { + includesSecurityGroupSource = true; + } + } + + // Add security group sources if they exist + if (includesSecurityGroupSource) { + const securityGroup = securityGroupMap.get(securityGroupItem.name); + + if (!securityGroup) { + throw new Error(`[network-vpc-stack] Unable to locate security group ${securityGroupItem.name}`); + } + + const ingressRules: SecurityGroupRuleProps[] = this.processSecurityGroupRules( + ingressRuleItem, + prefixListMap, + securityGroupMap, + ); + + for (const [index, ingressRule] of ingressRules.entries()) { + if (ingressRule.targetSecurityGroup) { + securityGroup.addIngressRule(`${securityGroupItem.name}-Ingress-${ruleId}-${index}`, { + sourceSecurityGroup: ingressRule.targetSecurityGroup, + ...ingressRule, + }); + } + } + } + } + + for (const [ruleId, egressRuleItem] of securityGroupItem.outboundRules.entries() ?? []) { + // Check if rule destinations include a security group reference + let includesSecurityGroupSource = false; + for (const source of egressRuleItem.sources) { + if (NetworkConfigTypes.securityGroupSourceConfig.is(source)) { + includesSecurityGroupSource = true; + } + } + + // Add security group sources if they exist + if (includesSecurityGroupSource) { + const securityGroup = securityGroupMap.get(securityGroupItem.name); + + if (!securityGroup) { + throw new Error(`[network-vpc-stack] Unable to locate security group ${securityGroupItem.name}`); + } + + const egressRules: SecurityGroupRuleProps[] = this.processSecurityGroupRules( + egressRuleItem, + prefixListMap, + securityGroupMap, + ); + + for (const [index, egressRule] of egressRules.entries()) { + if (egressRule.targetSecurityGroup) { + securityGroup.addEgressRule(`${securityGroupItem.name}-Egress-${ruleId}-${index}`, { + destinationSecurityGroup: egressRule.targetSecurityGroup, + ...egressRule, + }); + } + } + } + } + } + // + // Create NACLs + // + for (const naclItem of vpcItem.networkAcls ?? []) { + Logger.info(`[network-vpc-stack] Adding Network ACL ${naclItem.name}`); + + const networkAcl = new NetworkAcl(this, `${pascalCase(vpcItem.name)}Vpc${pascalCase(naclItem.name)}Nacl`, { + networkAclName: naclItem.name, + vpc, + tags: naclItem.tags, + }); + + new cdk.aws_ssm.StringParameter( + this, + pascalCase(`SsmParam${pascalCase(vpcItem.name)}${pascalCase(naclItem.name)}Nacl`), + { + parameterName: `/accelerator/network/vpc/${vpcItem.name}/networkAcl/${naclItem.name}/id`, + stringValue: networkAcl.networkAclId, + }, + ); + + for (const subnetItem of naclItem.subnetAssociations) { + Logger.info(`[network-vpc-stack] Associate ${naclItem.name} to subnet ${subnetItem}`); + const subnet = subnetMap.get(subnetItem); + if (subnet === undefined) { + throw new Error(`Subnet ${subnetItem} not defined`); + } + networkAcl.associateSubnet( + `${pascalCase(vpcItem.name)}Vpc${pascalCase(naclItem.name)}NaclAssociate${pascalCase(subnetItem)}`, + { + subnet, + }, + ); + } + + for (const inboundRuleItem of naclItem.inboundRules ?? []) { + Logger.info(`[network-vpc-stack] Adding inbound rule ${inboundRuleItem.rule} to ${naclItem.name}`); + const props: { cidrBlock?: string; ipv6CidrBlock?: string } = this.processNetworkAclTarget( + inboundRuleItem.source, + ); + + Logger.info(`[network-vpc-stack] Adding inbound entries`); + networkAcl.addEntry( + `${pascalCase(vpcItem.name)}Vpc${pascalCase(naclItem.name)}-Inbound-${inboundRuleItem.rule}`, + { + egress: false, + protocol: inboundRuleItem.protocol, + ruleAction: inboundRuleItem.action, + ruleNumber: inboundRuleItem.rule, + portRange: { + from: inboundRuleItem.fromPort, + to: inboundRuleItem.toPort, + }, + ...props, + }, + ); + } + + for (const outboundRuleItem of naclItem.outboundRules ?? []) { + Logger.info(`[network-vpc-stack] Adding outbound rule ${outboundRuleItem.rule} to ${naclItem.name}`); + const props: { cidrBlock?: string; ipv6CidrBlock?: string } = this.processNetworkAclTarget( + outboundRuleItem.destination, + ); + + Logger.info(`[network-vpc-stack] Adding outbound entries`); + networkAcl.addEntry( + `${pascalCase(vpcItem.name)}Vpc${pascalCase(naclItem.name)}-Outbound-${outboundRuleItem.rule}`, + { + egress: false, + protocol: outboundRuleItem.protocol, + ruleAction: outboundRuleItem.action, + ruleNumber: outboundRuleItem.rule, + portRange: { + from: outboundRuleItem.fromPort, + to: outboundRuleItem.toPort, + }, + ...props, + }, + ); + } + } + } + } + Logger.info('[network-vpc-stack] Completed stack synthesis'); + } + + private processNetworkAclTarget(target: string | NetworkAclSubnetSelection): { + cidrBlock?: string; + ipv6CidrBlock?: string; + } { + Logger.info(`[network-vpc-stack] processNetworkAclRules`); + + // + // IP target + // + if (nonEmptyString.is(target)) { + Logger.info(`[network-vpc-stack] Evaluate IP Target ${target}`); + if (target.includes('::')) { + return { ipv6CidrBlock: target }; + } else { + return { cidrBlock: target }; + } + } + + // + // Subnet Source target + // + if (NetworkConfigTypes.networkAclSubnetSelection.is(target)) { + Logger.info( + `[network-vpc-stack] Evaluate Subnet Source account:${target.account} vpc:${target.vpc} subnets:[${target.subnet}]`, + ); + + // Locate the VPC + const vpcItem = this.props.networkConfig.vpcs?.find( + item => item.account === target.account && item.name === target.vpc, + ); + if (vpcItem === undefined) { + throw new Error(`Specified VPC ${target.vpc} not defined`); + } + + // Locate the Subnet + const subnetItem = vpcItem.subnets?.find(item => item.name === target.subnet); + if (subnetItem === undefined) { + throw new Error(`Specified subnet ${target.subnet} not defined`); + } + return { cidrBlock: subnetItem.ipv4CidrBlock }; + } + + throw new Error(`Invalid input to processNetworkAclTargets`); + } + + private processSecurityGroupRules( + item: SecurityGroupRuleConfig, + prefixListMap: Map, + securityGroupMap?: Map, + ): SecurityGroupRuleProps[] { + const rules: SecurityGroupRuleProps[] = []; + + Logger.info(`[network-vpc-stack] processSecurityGroupRules`); + + if (!item.types) { + Logger.info(`[network-vpc-stack] types not defined, expecting tcpPorts and udpPorts to be set`); + for (const port of item.tcpPorts ?? []) { + Logger.debug(`[network-vpc-stack] Adding TCP port ${port}`); + rules.push( + ...this.processSecurityGroupRuleSources( + item.sources, + prefixListMap, + { + ipProtocol: cdk.aws_ec2.Protocol.TCP, + fromPort: port, + toPort: port, + description: item.description, + }, + securityGroupMap, + ), + ); + } + + for (const port of item.udpPorts ?? []) { + Logger.debug(`[network-vpc-stack] Adding UDP port ${port}`); + rules.push( + ...this.processSecurityGroupRuleSources( + item.sources, + prefixListMap, + { + ipProtocol: cdk.aws_ec2.Protocol.UDP, + fromPort: port, + toPort: port, + description: item.description, + }, + securityGroupMap, + ), + ); + } + } + + for (const type of item.types) { + Logger.info(`[network-vpc-stack] Adding type ${type}`); + if (type === 'ALL') { + rules.push( + ...this.processSecurityGroupRuleSources( + item.sources, + prefixListMap, + { + ipProtocol: cdk.aws_ec2.Protocol.ALL, + description: item.description, + }, + securityGroupMap, + ), + ); + } else if (Object.keys(TCP_PROTOCOLS_PORT).includes(type)) { + rules.push( + ...this.processSecurityGroupRuleSources( + item.sources, + prefixListMap, + { + ipProtocol: cdk.aws_ec2.Protocol.TCP, + fromPort: TCP_PROTOCOLS_PORT[type], + toPort: TCP_PROTOCOLS_PORT[type], + description: item.description, + }, + securityGroupMap, + ), + ); + } else { + rules.push( + ...this.processSecurityGroupRuleSources( + item.sources, + prefixListMap, + { + ipProtocol: type, + fromPort: item.fromPort, + toPort: item.toPort, + description: item.description, + }, + securityGroupMap, + ), + ); + } + } + + return rules; + } + + /** + * Processes individual security group source references. + * + * @param sources + * @param prefixListMap + * @param securityGroupMap + * @param props + * @returns + */ + private processSecurityGroupRuleSources( + sources: string[] | SecurityGroupSourceConfig[] | PrefixListSourceConfig[] | SubnetSourceConfig[], + prefixListMap: Map, + props: { + ipProtocol: string; + fromPort?: number; + toPort?: number; + description?: string; + }, + securityGroupMap?: Map, + ): SecurityGroupRuleProps[] { + const rules: SecurityGroupRuleProps[] = []; + + Logger.info(`[network-vpc-stack] processSecurityGroupRuleSources`); + + for (const source of sources ?? []) { + // Conditional to only process non-security group sources + if (!securityGroupMap) { + // + // IP source + // + if (nonEmptyString.is(source)) { + Logger.info(`[network-vpc-stack] Evaluate IP Source ${source}`); + if (source.includes('::')) { + rules.push({ + cidrIpv6: source, + ...props, + }); + } else { + rules.push({ + cidrIp: source, + ...props, + }); + } + } + + // + // Subnet source + // + if (NetworkConfigTypes.subnetSourceConfig.is(source)) { + Logger.info( + `[network-vpc-stack] Evaluate Subnet Source account:${source.account} vpc:${source.vpc} subnets:[${source.subnets}]`, + ); + + // Locate the VPC + const vpcItem = this.props.networkConfig.vpcs?.find( + item => item.account === source.account && item.name === source.vpc, + ); + if (!vpcItem) { + throw new Error(`Specified VPC ${source.vpc} not defined`); + } + + // Loop through all subnets to add + for (const subnet of source.subnets) { + // Locate the Subnet + const subnetItem = vpcItem.subnets?.find(item => item.name === subnet); + if (!subnetItem) { + throw new Error(`Specified subnet ${subnet} not defined`); + } + rules.push({ + // TODO: Add support for dynamic IP lookup + cidrIp: subnetItem.ipv4CidrBlock, + ...props, + }); + } + } + + // + // Prefix List Source + // + if (NetworkConfigTypes.prefixListSourceConfig.is(source)) { + Logger.info(`[network-vpc-stack] Evaluate Prefix List Source prefixLists:[${source.prefixLists}]`); + + for (const prefixList of source.prefixLists ?? []) { + const targetPrefixList = prefixListMap.get(prefixList); + if (!targetPrefixList) { + throw new Error(`Specified Prefix List ${prefixList} not defined`); + } + rules.push({ + targetPrefixList, + ...props, + }); + } + } + } + + if (securityGroupMap) { + // + // Security Group Source + // + if (NetworkConfigTypes.securityGroupSourceConfig.is(source)) { + Logger.info(`[network-vpc-stack] Evaluate Security Group Source securityGroups:[${source.securityGroups}]`); + + for (const securityGroup of source.securityGroups ?? []) { + const targetSecurityGroup = securityGroupMap.get(securityGroup); + if (!targetSecurityGroup) { + throw new Error(`Specified Security Group ${securityGroup} not defined`); + } + rules.push({ + targetSecurityGroup, + ...props, + }); + } + } + } + } + + return rules; + } + + /** + * Add RAM resource shares to the stack. + * + * @param item + * @param resourceShareName + * @param resourceArns + */ + private addResourceShare(item: ResourceShareType, resourceShareName: string, resourceArns: string[]) { + // Build a list of principals to share to + const principals: string[] = []; + + // Loop through all the defined OUs + for (const ouItem of item.shareTargets?.organizationalUnits ?? []) { + let ouArn = this.orgConfig.getOrganizationalUnitArn(ouItem); + // AWS::RAM::ResourceShare expects the organizations ARN if + // sharing with the entire org (Root) + if (ouItem === 'Root') { + ouArn = ouArn.substring(0, ouArn.lastIndexOf('/')).replace('root', 'organization'); + } + Logger.info(`[network-vpc-stack] Share ${resourceShareName} with Organizational Unit ${ouItem}: ${ouArn}`); + principals.push(ouArn); + } + + // Loop through all the defined accounts + for (const account of item.shareTargets?.accounts ?? []) { + const accountId = this.accountsConfig.getAccountId(account); + Logger.info(`[network-vpc-stack] Share ${resourceShareName} with Account ${account}: ${accountId}`); + principals.push(accountId); + } + + // Create the Resource Share + new ResourceShare(this, `${pascalCase(resourceShareName)}ResourceShare`, { + name: resourceShareName, + principals, + resourceArns: resourceArns, + }); + } + + /** + * Get the resource ID from a RAM share. + * + * @param resourceShareName + * @param itemType + * @param owningAccountId + */ + private getResourceShare(resourceShareName: string, itemType: string, owningAccountId: string): IResourceShareItem { + // Generate a logical ID + const resourceName = resourceShareName.split('_')[0]; + const logicalId = `${resourceName}${itemType.split(':')[1]}`; + + // Lookup resource share + const resourceShare = ResourceShare.fromLookup(this, pascalCase(`${logicalId}Share`), { + resourceShareOwner: ResourceShareOwner.OTHER_ACCOUNTS, + resourceShareName: resourceShareName, + owningAccountId, + }); + + // Represents the item shared by RAM + const item = ResourceShareItem.fromLookup(this, pascalCase(`${logicalId}`), { + resourceShare, + resourceShareItemType: itemType, + kmsKey: this.acceleratorKey, + logRetentionInDays: this.logRetention, + }); + return item; + } +} diff --git a/source/packages/@aws-accelerator/accelerator/lib/stacks/operations-stack.ts b/source/packages/@aws-accelerator/accelerator/lib/stacks/operations-stack.ts new file mode 100644 index 000000000..927a69375 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/lib/stacks/operations-stack.ts @@ -0,0 +1,276 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { NagSuppressions } from 'cdk-nag'; +import { pascalCase } from 'change-case'; +import { Construct } from 'constructs'; +import * as path from 'path'; + +import { Logger } from '../logger'; +import { AcceleratorStack, AcceleratorStackProps } from './accelerator-stack'; + +export interface OperationsStackProps extends AcceleratorStackProps { + configDirPath: string; +} + +export class OperationsStack extends AcceleratorStack { + constructor(scope: Construct, id: string, props: OperationsStackProps) { + super(scope, id, props); + + // + // Only deploy IAM resources into the home region + // + if (props.globalConfig.homeRegion === cdk.Stack.of(this).region) { + // + // Add Providers + // + const providers: { [name: string]: cdk.aws_iam.SamlProvider } = {}; + for (const providerItem of props.iamConfig.providers ?? []) { + Logger.info(`[operations-stack] Add Provider ${providerItem.name}`); + providers[providerItem.name] = new cdk.aws_iam.SamlProvider( + this, + `${pascalCase(providerItem.name)}SamlProvider`, + { + name: providerItem.name, + metadataDocument: cdk.aws_iam.SamlMetadataDocument.fromFile( + path.join(props.configDirPath, providerItem.metadataDocument), + ), + }, + ); + } + + // + // Add Managed Policies + // + const policies: { [name: string]: cdk.aws_iam.ManagedPolicy } = {}; + for (const policySetItem of props.iamConfig.policySets ?? []) { + if (!this.isIncluded(policySetItem.deploymentTargets)) { + Logger.info(`[operations-stack] Item excluded`); + continue; + } + + for (const policyItem of policySetItem.policies) { + Logger.info(`[operations-stack] Add customer managed policy ${policyItem.name}`); + + // Read in the policy document which should be properly formatted json + const policyDocument = require(path.join(props.configDirPath, policyItem.policy)); + + // Create a statements list using the PolicyStatement factory + const statements: cdk.aws_iam.PolicyStatement[] = []; + for (const statement of policyDocument.Statement) { + statements.push(cdk.aws_iam.PolicyStatement.fromJson(statement)); + } + + // Construct the ManagedPolicy + policies[policyItem.name] = new cdk.aws_iam.ManagedPolicy(this, pascalCase(policyItem.name), { + managedPolicyName: policyItem.name, + statements, + }); + + // AwsSolutions-IAM5: The IAM entity contains wildcard permissions and does not have a cdk_nag rule suppression with evidence for those permission + // rule suppression with evidence for this permission. + NagSuppressions.addResourceSuppressionsByPath( + this, + `${this.stackName}/${pascalCase(policyItem.name)}/Resource`, + [ + { + id: 'AwsSolutions-IAM5', + reason: 'Policies definition are derived from accelerator iam-config boundary-policy file', + }, + ], + ); + } + } + + // + // Add Roles + // + const roles: { [name: string]: cdk.aws_iam.Role } = {}; + for (const roleSetItem of props.iamConfig.roleSets ?? []) { + if (!this.isIncluded(roleSetItem.deploymentTargets)) { + Logger.info(`[operations-stack] Item excluded`); + continue; + } + + for (const roleItem of roleSetItem.roles) { + Logger.info(`[operations-stack] Add role ${roleItem.name}`); + + const principals: cdk.aws_iam.PrincipalBase[] = []; + + for (const assumedByItem of roleItem.assumedBy ?? []) { + Logger.info( + `[operations-stack] Role - assumed by type(${assumedByItem.type}) principal(${assumedByItem.principal})`, + ); + + if (assumedByItem.type === 'service') { + principals.push(new cdk.aws_iam.ServicePrincipal(assumedByItem.principal)); + } + + if (assumedByItem.type === 'account') { + principals.push(new cdk.aws_iam.AccountPrincipal(assumedByItem.principal)); + } + + if (assumedByItem.type === 'provider') { + principals.push(new cdk.aws_iam.SamlConsolePrincipal(providers[assumedByItem.principal])); + } + } + + const managedPolicies: cdk.aws_iam.IManagedPolicy[] = []; + for (const policyItem of roleItem.policies?.awsManaged ?? []) { + Logger.info(`[operations-stack] Role - aws managed policy ${policyItem}`); + managedPolicies.push(cdk.aws_iam.ManagedPolicy.fromAwsManagedPolicyName(policyItem)); + } + for (const policyItem of roleItem.policies?.customerManaged ?? []) { + Logger.info(`[operations-stack] Role - customer managed policy ${policyItem}`); + managedPolicies.push(policies[policyItem]); + } + + let assumedBy: cdk.aws_iam.IPrincipal; + if (roleItem.assumedBy.find(item => item.type === 'provider')) { + // Since a SamlConsolePrincipal creates conditions, we can not + // use the CompositePrincipal. Verify that it is alone + if (principals.length > 1) { + throw new Error('More than one principal found when adding provider'); + } + assumedBy = principals[0]; + } else { + assumedBy = new cdk.aws_iam.CompositePrincipal(...principals); + } + + const role = new cdk.aws_iam.Role(this, pascalCase(roleItem.name), { + roleName: roleItem.name, + assumedBy, + managedPolicies, + permissionsBoundary: policies[roleItem.boundaryPolicy], + }); + + // AwsSolutions-IAM4: The IAM user, role, or group uses AWS managed policies + // rule suppression with evidence for this permission. + NagSuppressions.addResourceSuppressionsByPath( + this, + `${this.stackName}/${pascalCase(roleItem.name)}/Resource`, + [ + { + id: 'AwsSolutions-IAM4', + reason: 'IAM Role created as per accelerator iam-config needs AWS managed policy', + }, + ], + ); + + // Create instance profile + if (roleItem.instanceProfile) { + Logger.info(`[operations-stack] Role - creating instance profile for ${roleItem.name}`); + new cdk.aws_iam.CfnInstanceProfile(this, `${pascalCase(roleItem.name)}InstanceProfile`, { + // Use role object to force use of Ref + instanceProfileName: role.roleName, + roles: [role.roleName], + }); + } + + // Add to roles list + roles[roleItem.name] = role; + } + } + + // + // Add Groups + // + const groups: { [name: string]: cdk.aws_iam.Group } = {}; + for (const groupSetItem of props.iamConfig.groupSets ?? []) { + if (!this.isIncluded(groupSetItem.deploymentTargets)) { + Logger.info(`[operations-stack] Item excluded`); + continue; + } + + for (const groupItem of groupSetItem.groups) { + Logger.info(`[operations-stack] Add group ${groupItem.name}`); + + const managedPolicies: cdk.aws_iam.IManagedPolicy[] = []; + for (const policyItem of groupItem.policies?.awsManaged ?? []) { + Logger.info(`[operations-stack] Group - aws managed policy ${policyItem}`); + managedPolicies.push(cdk.aws_iam.ManagedPolicy.fromAwsManagedPolicyName(policyItem)); + } + for (const policyItem of groupItem.policies?.customerManaged ?? []) { + Logger.info(`[operations-stack] Group - customer managed policy ${policyItem}`); + managedPolicies.push(policies[policyItem]); + } + + groups[groupItem.name] = new cdk.aws_iam.Group(this, pascalCase(groupItem.name), { + groupName: groupItem.name, + managedPolicies, + }); + + // AwsSolutions-IAM4: The IAM user, role, or group uses AWS managed policies + // rule suppression with evidence for this permission. + NagSuppressions.addResourceSuppressionsByPath( + this, + `${this.stackName}/${pascalCase(groupItem.name)}/Resource`, + [ + { + id: 'AwsSolutions-IAM4', + reason: 'Groups created as per accelerator iam-config needs AWS managed policy', + }, + ], + ); + } + } + + // + // Add IAM Users + // + for (const userSet of props.iamConfig.userSets ?? []) { + if (!this.isIncluded(userSet.deploymentTargets)) { + Logger.info(`[operations-stack] Item excluded`); + continue; + } + + for (const user of userSet.users ?? []) { + Logger.info(`[operations-stack] Add user ${user.username}`); + + const secret = new cdk.aws_secretsmanager.Secret(this, pascalCase(`${user.username}Secret`), { + generateSecretString: { + secretStringTemplate: JSON.stringify({ username: user.username }), + generateStringKey: 'password', + }, + secretName: `/accelerator/${user.username}`, + }); + + // AwsSolutions-SMG4: The secret does not have automatic rotation scheduled. + // rule suppression with evidence for this permission. + NagSuppressions.addResourceSuppressionsByPath( + this, + `${this.stackName}/${pascalCase(user.username)}Secret/Resource`, + [ + { + id: 'AwsSolutions-SMG4', + reason: + 'Accelerator users created as per iam-config file, upcoming change will take care of secret automatic rotation', + }, + ], + ); + + Logger.info(`[operations-stack] User - password stored to /accelerator/${user.username}`); + + new cdk.aws_iam.User(this, pascalCase(user.username), { + userName: user.username, + password: secret.secretValue, + groups: [groups[user.group]], + permissionsBoundary: policies[user.boundaryPolicy], + }); + } + } + } + Logger.info('[operations-stack] Completed stack synthesis'); + } +} diff --git a/source/packages/@aws-accelerator/accelerator/lib/stacks/organizations-stack.ts b/source/packages/@aws-accelerator/accelerator/lib/stacks/organizations-stack.ts new file mode 100644 index 000000000..ff2c2bcce --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/lib/stacks/organizations-stack.ts @@ -0,0 +1,475 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { NagSuppressions } from 'cdk-nag'; +import * as cloudtrail from 'aws-cdk-lib/aws-cloudtrail'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as kms from 'aws-cdk-lib/aws-kms'; +import * as logs from 'aws-cdk-lib/aws-logs'; +import * as s3 from 'aws-cdk-lib/aws-s3'; +import { Construct } from 'constructs'; +import { pascalCase } from 'pascal-case'; +import * as path from 'path'; + +import { Region } from '@aws-accelerator/config'; +import { + Bucket, + BucketEncryptionType, + BudgetDefinition, + EnableAwsServiceAccess, + EnablePolicyType, + EnableSharingWithAwsOrganization, + GuardDutyOrganizationAdminAccount, + KeyLookup, + MacieOrganizationAdminAccount, + Policy, + PolicyAttachment, + PolicyType, + PolicyTypeEnum, + RegisterDelegatedAdministrator, + ReportDefinition, + SecurityHubOrganizationAdminAccount, +} from '@aws-accelerator/constructs'; +import * as cdk_extensions from '@aws-cdk-extensions/cdk-extensions'; + +import { Logger } from '../logger'; +import { AcceleratorStack, AcceleratorStackProps } from './accelerator-stack'; +import { KeyStack } from './key-stack'; +import { S3ServerAccessLogsBucketNamePrefix } from '../accelerator'; +import { LifecycleRule } from '@aws-accelerator/constructs/lib/aws-s3/bucket'; + +export interface OrganizationsStackProps extends AcceleratorStackProps { + configDirPath: string; +} + +/** + * The Organizations stack is executed in all enabled regions in the + * Organizations Management (Root) account + */ +export class OrganizationsStack extends AcceleratorStack { + constructor(scope: Construct, id: string, props: OrganizationsStackProps) { + super(scope, id, props); + + Logger.debug(`[organizations-stack] homeRegion: ${props.globalConfig.homeRegion}`); + + const key = new KeyLookup(this, 'AcceleratorKeyLookup', { + accountId: props.accountsConfig.getAuditAccountId(), + roleName: KeyStack.CROSS_ACCOUNT_ACCESS_ROLE_NAME, + keyArnParameterName: KeyStack.ACCELERATOR_KEY_ARN_PARAMETER_NAME, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }).getKey(); + + // + // Global Organizations actions, only execute in the home region + // + if (props.globalConfig.homeRegion === cdk.Stack.of(this).region) { + // + // Configure Organizations Trail + // + Logger.debug(`[organizations-stack] logging.cloudtrail.enable: ${props.globalConfig.logging.cloudtrail.enable}`); + Logger.debug( + `[organizations-stack] logging.cloudtrail.organizationTrail: ${props.globalConfig.logging.cloudtrail.organizationTrail}`, + ); + + if (props.globalConfig.logging.cloudtrail.enable && props.globalConfig.logging.cloudtrail.organizationTrail) { + Logger.info('[organizations-stack] Adding Organizations CloudTrail'); + + const enableCloudtrailServiceAccess = new EnableAwsServiceAccess(this, 'EnableOrganizationsCloudTrail', { + servicePrincipal: 'cloudtrail.amazonaws.com', + kmsKey: key, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }); + + const cloudTrailCloudWatchCmk = new kms.Key(this, 'CloudTrailCloudWatchCmk', { + enableKeyRotation: true, + description: 'CloudTrail Log Group CMK', + alias: 'accelerator/organizations-cloudtrail/log-group/', + }); + cloudTrailCloudWatchCmk.addToResourcePolicy( + new iam.PolicyStatement({ + sid: 'Allow Account use of the key', + actions: ['kms:*'], + principals: [new iam.AccountRootPrincipal()], + resources: ['*'], + }), + ); + cloudTrailCloudWatchCmk.addToResourcePolicy( + new iam.PolicyStatement({ + sid: 'Allow logs use of the key', + actions: ['kms:*'], + principals: [new iam.ServicePrincipal(`logs.${cdk.Stack.of(this).region}.amazonaws.com`)], + resources: ['*'], + conditions: { + ArnEquals: { + 'kms:EncryptionContext:aws:logs:arn': `arn:${cdk.Stack.of(this).partition}:logs:${ + cdk.Stack.of(this).region + }:${cdk.Stack.of(this).account}:*`, + }, + }, + }), + ); + + const cloudTrailCloudWatchCmkLogGroup = new logs.LogGroup(this, 'CloudTrailCloudWatchLogGroup', { + retention: props.globalConfig.cloudwatchLogRetentionInDays, + encryptionKey: cloudTrailCloudWatchCmk, + logGroupName: 'aws-accelerator-cloudtrail-logs', + }); + + const organizationsTrail = new cdk_extensions.Trail(this, 'OrganizationsCloudTrail', { + bucket: s3.Bucket.fromBucketName( + this, + 'CentralLogsBucket', + `aws-accelerator-central-logs-${props.accountsConfig.getLogArchiveAccountId()}-${ + cdk.Stack.of(this).region + }`, + ), + cloudWatchLogGroup: cloudTrailCloudWatchCmkLogGroup, + cloudWatchLogsRetention: logs.RetentionDays.TEN_YEARS, + enableFileValidation: true, + encryptionKey: kms.Key.fromKeyArn( + this, + 'CentralLogsCmk', + `arn:${cdk.Stack.of(this).partition}:kms:${ + cdk.Stack.of(this).region + }:${props.accountsConfig.getLogArchiveAccountId()}:alias/accelerator/central-logs/s3`, + ), + includeGlobalServiceEvents: true, + isMultiRegionTrail: true, + isOrganizationTrail: true, + managementEvents: cloudtrail.ReadWriteType.ALL, + sendToCloudWatchLogs: true, + trailName: 'AWSAccelerator-Organizations-CloudTrail', + }); + + organizationsTrail.addEventSelector(cloudtrail.DataResourceType.S3_OBJECT, [ + `arn:${cdk.Stack.of(this).partition}:s3:::`, + ]); + organizationsTrail.addEventSelector(cloudtrail.DataResourceType.LAMBDA_FUNCTION, [ + `arn:${cdk.Stack.of(this).partition}:lambda`, + ]); + + organizationsTrail.node.addDependency(enableCloudtrailServiceAccess); + } + + // + // Enable Backup Policy + // + if (props.organizationConfig.backupPolicies.length > 0) { + Logger.info(`[organizations-stack] Adding Backup Policies`); + + const role = new cdk.aws_iam.Role(this, 'BackupRole', { + roleName: 'Backup-Role', + assumedBy: new cdk.aws_iam.ServicePrincipal('backup.amazonaws.com'), + }); + + const managedBackupPolicy = cdk.aws_iam.ManagedPolicy.fromAwsManagedPolicyName( + 'service-role/AWSBackupServiceRolePolicyForBackup', + ); + role.addManagedPolicy(managedBackupPolicy); + + // AwsSolutions-IAM4: The IAM user, role, or group uses AWS managed policies + // rule suppression with evidence for this permission. + NagSuppressions.addResourceSuppressionsByPath(this, `${this.stackName}/BackupRole/Resource`, [ + { + id: 'AwsSolutions-IAM4', + reason: + 'BackupRole needs service-role/AWSBackupServiceRolePolicyForBackup managed policy to manage backup vault', + }, + ]); + + const vault = new cdk.aws_backup.BackupVault(this, 'BackupVault', { + backupVaultName: 'BackupVault', + }); + + vault.node.addDependency(role); + + new EnablePolicyType(this, 'enablePolicyBackup', { + policyType: PolicyTypeEnum.BACKUP_POLICY, + kmsKey: key, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }); + + for (const backupPolicies of props.organizationConfig.backupPolicies ?? []) { + for (const orgUnit of backupPolicies.deploymentTargets.organizationalUnits) { + const policy = new Policy(this, backupPolicies.name, { + description: backupPolicies.description, + name: backupPolicies.name, + path: path.join(props.configDirPath, backupPolicies.policy), + type: PolicyType.BACKUP_POLICY, + kmsKey: key, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + acceleratorPrefix: 'AWSAccelerator', + managementAccountAccessRole: props.globalConfig.managementAccountAccessRole, + }); + + policy.node.addDependency(vault); + + new PolicyAttachment(this, pascalCase(`Attach_${backupPolicies.name}_${orgUnit}`), { + policyId: policy.id, + targetId: props.organizationConfig.getOrganizationalUnitId(orgUnit), + type: PolicyType.BACKUP_POLICY, + kmsKey: key, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }); + } + } + } + + // + // Enable Cost and Usage Reports + // + if (props.globalConfig.reports?.costAndUsageReport) { + Logger.info('[organizations-stack] Adding Cost and Usage Reports'); + + const lifecycleRules: LifecycleRule[] = []; + for (const lifecycleRule of props.globalConfig.reports.costAndUsageReport.lifecycleRules ?? []) { + const noncurrentVersionTransitions = []; + for (const noncurrentVersionTransition of lifecycleRule.noncurrentVersionTransitions) { + noncurrentVersionTransitions.push({ + storageClass: noncurrentVersionTransition.storageClass, + transitionAfter: noncurrentVersionTransition.transitionAfter, + }); + } + const transitions = []; + for (const transition of lifecycleRule.transitions) { + transitions.push({ + storageClass: transition.storageClass, + transitionAfter: transition.transitionAfter, + }); + } + const rule: LifecycleRule = { + abortIncompleteMultipartUploadAfter: lifecycleRule.abortIncompleteMultipartUpload, + enabled: lifecycleRule.enabled, + expiration: lifecycleRule.expiration, + expiredObjectDeleteMarker: lifecycleRule.expiredObjectDeleteMarker, + id: lifecycleRule.id, + noncurrentVersionExpiration: lifecycleRule.noncurrentVersionExpiration, + noncurrentVersionTransitions, + transitions, + }; + lifecycleRules.push(rule); + } + + const reportBucket = new Bucket(this, 'ReportBucket', { + encryptionType: BucketEncryptionType.SSE_S3, // CUR does not support KMS CMK + s3BucketName: `aws-accelerator-cur-${cdk.Stack.of(this).account}-${cdk.Stack.of(this).region}`, + serverAccessLogsBucketName: `${S3ServerAccessLogsBucketNamePrefix}-${cdk.Aws.ACCOUNT_ID}-${cdk.Aws.REGION}`, + lifecycleRules, + }); + + new ReportDefinition(this, 'ReportDefinition', { + compression: props.globalConfig.reports.costAndUsageReport.compression, + format: props.globalConfig.reports.costAndUsageReport.format, + refreshClosedReports: props.globalConfig.reports.costAndUsageReport.refreshClosedReports, + reportName: props.globalConfig.reports.costAndUsageReport.reportName, + reportVersioning: props.globalConfig.reports.costAndUsageReport.reportVersioning, + s3Bucket: reportBucket.getS3Bucket(), + s3Prefix: props.globalConfig.reports.costAndUsageReport.s3Prefix, + s3Region: cdk.Stack.of(this).region, + timeUnit: props.globalConfig.reports.costAndUsageReport.timeUnit, + additionalArtifacts: props.globalConfig.reports.costAndUsageReport.additionalArtifacts, + additionalSchemaElements: props.globalConfig.reports.costAndUsageReport.additionalSchemaElements, + kmsKey: key, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }); + } + // + // Enable Budget Reports + // + if (props.globalConfig.reports?.budgets) { + Logger.info('[organizations-stack] Adding Budget Reports'); + + new BudgetDefinition(this, 'BudgetDefinition', { + budgets: props.globalConfig.reports.budgets, + }); + } + // + // IAM Access Analyzer (Does not have a native service enabler) + // + if (props.securityConfig.accessAnalyzer.enable) { + Logger.debug('[organizations-stack] Enable Service Access for access-analyzer.amazonaws.com'); + + const role = new iam.CfnServiceLinkedRole(this, 'AccessAnalyzerServiceLinkedRole', { + awsServiceName: 'access-analyzer.amazonaws.com', + }); + + const enableAccessAnalyzer = new EnableAwsServiceAccess(this, 'EnableAccessAnalyzer', { + servicePrincipal: 'access-analyzer.amazonaws.com', + kmsKey: key, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }); + + enableAccessAnalyzer.node.addDependency(role); + + const registerDelegatedAdministratorAccessAnalyzer = new RegisterDelegatedAdministrator( + this, + 'RegisterDelegatedAdministratorAccessAnalyzer', + { + accountId: props.accountsConfig.getAuditAccountId(), + servicePrincipal: 'access-analyzer.amazonaws.com', + kmsKey: key, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }, + ); + + registerDelegatedAdministratorAccessAnalyzer.node.addDependency(enableAccessAnalyzer); + } + + // + // Enable RAM organization sharing + // + if (props.organizationConfig.enable) { + new EnableSharingWithAwsOrganization(this, 'EnableSharingWithAwsOrganization', { + kmsKey: key, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }); + } + } + + // Security Services delegated admin account configuration + // Global decoration for security services + const delegatedAdminAccount = props.securityConfig.centralSecurityServices.delegatedAdminAccount; + const adminAccountId = props.accountsConfig.getAccountId(delegatedAdminAccount); + + // Macie Configuration + if (props.securityConfig.centralSecurityServices.macie.enable) { + if ( + props.securityConfig.centralSecurityServices.macie.excludeRegions!.indexOf( + cdk.Stack.of(this).region as Region, + ) == -1 + ) { + Logger.debug( + `[organizations-stack] Starts macie admin account delegation to the account with email ${ + props.accountsConfig.getAuditAccount().email + } account in ${cdk.Stack.of(this).region} region`, + ); + + Logger.debug(`[organizations-stack] Macie Admin Account ID is ${adminAccountId}`); + new MacieOrganizationAdminAccount(this, 'MacieOrganizationAdminAccount', { + adminAccountId: adminAccountId, + kmsKey: key, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }); + } else { + Logger.debug( + `[organizations-stack] ${ + cdk.Stack.of(this).region + } region was in macie excluded list so ignoring this region for ${ + props.accountsConfig.getAuditAccount().email + } account`, + ); + } + } + + //GuardDuty Config + if (props.securityConfig.centralSecurityServices.guardduty.enable) { + if ( + props.securityConfig.centralSecurityServices.guardduty.excludeRegions!.indexOf( + cdk.Stack.of(this).region as Region, + ) == -1 + ) { + Logger.debug( + `[organizations-stack] Starts guardduty admin account delegation to the account with email ${ + props.accountsConfig.getAuditAccount().email + } account in ${cdk.Stack.of(this).region} region`, + ); + + Logger.debug(`[organizations-stack] Guardduty Admin Account ID is ${adminAccountId}`); + new GuardDutyOrganizationAdminAccount(this, 'GuardDutyEnableOrganizationAdminAccount', { + adminAccountId: adminAccountId, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + kmsKey: key, + }); + } else { + Logger.debug( + `[organizations-stack] ${ + cdk.Stack.of(this).region + } region was in guardduty excluded list so ignoring this region for ${ + props.accountsConfig.getAuditAccount().email + } account`, + ); + } + } + + //SecurityHub Config + if (props.securityConfig.centralSecurityServices.securityHub.enable) { + if ( + props.securityConfig.centralSecurityServices.securityHub.excludeRegions!.indexOf( + cdk.Stack.of(this).region as Region, + ) == -1 + ) { + Logger.debug( + `[organizations-stack] Starts SecurityHub admin account delegation to the account with email ${ + props.accountsConfig.getAuditAccount().email + } account in ${cdk.Stack.of(this).region} region`, + ); + + Logger.debug(`[organizations-stack] SecurityHub Admin Account ID is ${adminAccountId}`); + new SecurityHubOrganizationAdminAccount(this, 'SecurityHubOrganizationAdminAccount', { + adminAccountId: adminAccountId, + kmsKey: key, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }); + } else { + Logger.debug( + `[organizations-stack] ${ + cdk.Stack.of(this).region + } region was in SecurityHub excluded list so ignoring this region for ${ + props.accountsConfig.getAuditAccount().email + } account`, + ); + } + } + // + // Tagging Policies Config + // + if (props.organizationConfig.taggingPolicies.length > 0) { + Logger.info(`[organizations-stack] Adding Tagging Policies`); + const tagPolicy = new EnablePolicyType(this, 'enablePolicyTypeTag', { + policyType: PolicyTypeEnum.TAG_POLICY, + kmsKey: key, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }); + for (const taggingPolicy of props.organizationConfig.taggingPolicies ?? []) { + for (const orgUnit of taggingPolicy.deploymentTargets.organizationalUnits) { + const policy = new Policy(this, taggingPolicy.name, { + description: taggingPolicy.description, + name: taggingPolicy.name, + path: path.join(props.configDirPath, taggingPolicy.policy), + type: PolicyType.TAG_POLICY, + kmsKey: key, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + acceleratorPrefix: 'AWSAccelerator', + managementAccountAccessRole: props.globalConfig.managementAccountAccessRole, + }); + + policy.node.addDependency(tagPolicy); + + new PolicyAttachment(this, pascalCase(`Attach_${taggingPolicy.name}_${orgUnit}`), { + policyId: policy.id, + targetId: props.organizationConfig.getOrganizationalUnitId(orgUnit), + type: PolicyType.TAG_POLICY, + kmsKey: key, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }); + } + } + } + // + // Configure Trusted Services and Delegated Management Accounts + // + // + Logger.info('[organizations-stack] Completed stack synthesis'); + } +} diff --git a/source/packages/@aws-accelerator/accelerator/lib/stacks/pipeline-stack.ts b/source/packages/@aws-accelerator/accelerator/lib/stacks/pipeline-stack.ts new file mode 100644 index 000000000..f4491382c --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/lib/stacks/pipeline-stack.ts @@ -0,0 +1,145 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { NagSuppressions } from 'cdk-nag'; +import { Construct } from 'constructs'; +import { version } from '../../../../../package.json'; +import * as pipeline from '../pipeline'; + +export interface PipelineStackProps extends cdk.StackProps { + readonly sourceRepository: string; + readonly sourceRepositoryOwner: string; + readonly sourceRepositoryName: string; + readonly sourceBranchName: string; + readonly enableApprovalStage: boolean; + readonly qualifier?: string; + readonly managementAccountId?: string; + readonly managementAccountRoleName?: string; + readonly managementAccountEmail: string; + readonly logArchiveAccountEmail: string; + readonly auditAccountEmail: string; + /** + * List of email addresses to be notified when pipeline is waiting for manual approval stage. + * If pipeline do not have approval stage enabled, this value will have no impact. + */ + readonly approvalStageNotifyEmailList?: string; + readonly partition: string; +} + +export class PipelineStack extends cdk.Stack { + constructor(scope: Construct, id: string, props: PipelineStackProps) { + super(scope, id, props); + + new cdk.aws_ssm.StringParameter(this, 'SsmParamStackId', { + parameterName: `/accelerator/${cdk.Stack.of(this).stackName}/stack-id`, + stringValue: cdk.Stack.of(this).stackId, + }); + + new cdk.aws_ssm.StringParameter(this, 'SsmParamAcceleratorVersion', { + parameterName: `/accelerator/${cdk.Stack.of(this).stackName}/version`, + stringValue: version, + }); + + const toolkitRole = new cdk.aws_iam.Role(this, 'AdminCdkToolkitRole', { + assumedBy: new cdk.aws_iam.ServicePrincipal('codebuild.amazonaws.com'), + managedPolicies: [cdk.aws_iam.ManagedPolicy.fromAwsManagedPolicyName('AdministratorAccess')], + }); + + // TODO: Add event to launch the Pipeline for new account events + new pipeline.AcceleratorPipeline(this, 'Pipeline', { + toolkitRole, + ...props, + }); + + // AwsSolutions-IAM5: The IAM entity contains wildcard permissions and does not have a cdk_nag rule suppression with evidence for those permission. + NagSuppressions.addResourceSuppressionsByPath( + this, + `${this.stackName}/Pipeline/PipelineRole/DefaultPolicy/Resource`, + [ + { + id: 'AwsSolutions-IAM5', + reason: 'PipelineRole DefaultPolicy is built by cdk.', + }, + ], + ); + + // AwsSolutions-IAM5: The IAM entity contains wildcard permissions and does not have a cdk_nag rule suppression with evidence for those permission. + NagSuppressions.addResourceSuppressionsByPath( + this, + `${this.stackName}/Pipeline/Resource/Source/Source/CodePipelineActionRole/DefaultPolicy/Resource`, + [ + { + id: 'AwsSolutions-IAM5', + reason: 'Source code pipeline action DefaultPolicy is built by cdk.', + }, + ], + ); + + // AwsSolutions-IAM5: The IAM entity contains wildcard permissions and does not have a cdk_nag rule suppression with evidence for those permission. + NagSuppressions.addResourceSuppressionsByPath( + this, + `${this.stackName}/Pipeline/Resource/Source/Configuration/CodePipelineActionRole/DefaultPolicy/Resource`, + [ + { + id: 'AwsSolutions-IAM5', + reason: 'Configuration source pipeline action DefaultPolicy is built by cdk.', + }, + ], + ); + + // AwsSolutions-IAM5: The IAM entity contains wildcard permissions and does not have a cdk_nag rule suppression with evidence for those permission + NagSuppressions.addResourceSuppressionsByPath(this, `${this.stackName}/Pipeline/BuildRole/DefaultPolicy/Resource`, [ + { + id: 'AwsSolutions-IAM5', + reason: 'Pipeline code build role is built by cdk.', + }, + ]); + + // AwsSolutions-IAM4: The IAM user, role, or group uses AWS managed policies. + NagSuppressions.addResourceSuppressionsByPath(this, `${this.stackName}/AdminCdkToolkitRole/Resource`, [ + { + id: 'AwsSolutions-IAM4', + reason: 'Pipeline toolkit project role is built by cdk.', + }, + ]); + + // AwsSolutions-IAM5: The IAM entity contains wildcard permissions and does not have a cdk_nag rule suppression with evidence for those permission. + NagSuppressions.addResourceSuppressionsByPath( + this, + `${this.stackName}/AdminCdkToolkitRole/DefaultPolicy/Resource`, + [ + { + id: 'AwsSolutions-IAM5', + reason: 'Pipeline toolkit project role DefaultPolicy is built by cdk.', + }, + ], + ); + + // AwsSolutions-CB3: The CodeBuild project has privileged mode enabled. + NagSuppressions.addResourceSuppressionsByPath(this, `${this.stackName}/Pipeline/ToolkitProject/Resource`, [ + { + id: 'AwsSolutions-CB3', + reason: 'Pipeline toolkit project allow access to the Docker daemon.', + }, + ]); + + // AwsSolutions-CB3: The CodeBuild project has privileged mode enabled. + NagSuppressions.addResourceSuppressionsByPath(this, `${this.stackName}/Pipeline/BuildProject/Resource`, [ + { + id: 'AwsSolutions-CB3', + reason: 'Pipeline build project allow access to the Docker daemon.', + }, + ]); + } +} diff --git a/source/packages/@aws-accelerator/accelerator/lib/stacks/prepare-stack.ts b/source/packages/@aws-accelerator/accelerator/lib/stacks/prepare-stack.ts new file mode 100644 index 000000000..e98f31e2a --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/lib/stacks/prepare-stack.ts @@ -0,0 +1,415 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { NagSuppressions } from 'cdk-nag'; +import { Construct } from 'constructs'; + +import { + CreateControlTowerAccounts, + CreateOrganizationAccounts, + GetPortfolioId, + OrganizationalUnits, +} from '@aws-accelerator/constructs'; + +import { Logger } from '../logger'; +import { ValidateEnvironmentConfig } from '../validate-environment-config'; +import { LoadAcceleratorConfigTable } from '../load-config-table'; +import { AcceleratorStack, AcceleratorStackProps } from './accelerator-stack'; +import path from 'path'; + +export class PrepareStack extends AcceleratorStack { + public static readonly MANAGEMENT_KEY_ARN_PARAMETER_NAME = '/accelerator/management/kms/key-arn'; + + constructor(scope: Construct, id: string, props: AcceleratorStackProps) { + super(scope, id, props); + if ( + cdk.Stack.of(this).region === props.globalConfig.homeRegion && + cdk.Stack.of(this).account === props.accountsConfig.getManagementAccountId() + ) { + Logger.debug(`[prepare-stack] homeRegion: ${props.globalConfig.homeRegion}`); + new cdk.aws_ssm.StringParameter(this, 'Parameter', { + parameterName: `/accelerator/prepare-stack/validate`, + stringValue: 'value', + }); + + const key = new cdk.aws_kms.Key(this, 'ManagementKey', { + alias: 'alias/accelerator/management/kms/key', + description: 'AWS Accelerator Management Account Kms Key', + enableKeyRotation: true, + removalPolicy: cdk.RemovalPolicy.RETAIN, + }); + + // Make assets from the configuration directory + const accountConfigAsset = new cdk.aws_s3_assets.Asset(this, 'AccountConfigAsset', { + path: path.join(props.configDirPath, 'accounts-config.yaml'), + }); + const organzationsConfigAsset = new cdk.aws_s3_assets.Asset(this, 'OrganizationConfigAsset', { + path: path.join(props.configDirPath, 'organization-config.yaml'), + }); + + // Allow Accelerator Role to use the encryption key + key.addToResourcePolicy( + new cdk.aws_iam.PolicyStatement({ + sid: `Allow Accelerator Role in this account to use the encryption key`, + principals: [new cdk.aws_iam.AnyPrincipal()], + actions: ['kms:Encrypt', 'kms:Decrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*', 'kms:DescribeKey'], + resources: ['*'], + conditions: { + ArnLike: { + 'aws:PrincipalARN': [ + `arn:${cdk.Stack.of(this).partition}:iam::${cdk.Stack.of(this).account}:role/AWSAccelerator-*`, + ], + }, + }, + }), + ); + + // Allow Cloudwatch logs to use the encryption key + key.addToResourcePolicy( + new cdk.aws_iam.PolicyStatement({ + sid: `Allow Cloudwatch logs to use the encryption key`, + principals: [new cdk.aws_iam.ServicePrincipal(`logs.${cdk.Stack.of(this).region}.amazonaws.com`)], + actions: ['kms:Encrypt*', 'kms:Decrypt*', 'kms:ReEncrypt*', 'kms:GenerateDataKey*', 'kms:Describe*'], + resources: ['*'], + conditions: { + ArnLike: { + 'kms:EncryptionContext:aws:logs:arn': `arn:${cdk.Stack.of(this).partition}:logs:${ + cdk.Stack.of(this).region + }:${cdk.Stack.of(this).account}:log-group:*`, + }, + }, + }), + ); + + new cdk.aws_ssm.StringParameter(this, 'AcceleratorManagementKmsArnParameter', { + parameterName: PrepareStack.MANAGEMENT_KEY_ARN_PARAMETER_NAME, + stringValue: key.keyArn, + }); + + const configTable = new cdk.aws_dynamodb.Table(this, 'AcceleratorConfigTable', { + partitionKey: { name: 'dataType', type: cdk.aws_dynamodb.AttributeType.STRING }, + sortKey: { name: 'acceleratorKey', type: cdk.aws_dynamodb.AttributeType.STRING }, + billingMode: cdk.aws_dynamodb.BillingMode.PAY_PER_REQUEST, + encryption: cdk.aws_dynamodb.TableEncryption.CUSTOMER_MANAGED, + encryptionKey: key, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + configTable.addLocalSecondaryIndex({ + indexName: 'awsResourceKeys', + sortKey: { name: 'awsKey', type: cdk.aws_dynamodb.AttributeType.STRING }, + projectionType: cdk.aws_dynamodb.ProjectionType.KEYS_ONLY, + }); + + // AwsSolutions-DDB3: The DynamoDB table does not have Point-in-time Recovery enabled. + NagSuppressions.addResourceSuppressionsByPath(this, `${this.stackName}/AcceleratorConfigTable/Resource`, [ + { + id: 'AwsSolutions-DDB3', + reason: 'AcceleratorConfigTable DynamoDB table do not need point in time recovery, data can be re-created', + }, + ]); + + Logger.info(`[prepare-stack] Load Config Table`); + const configRepoName = props.qualifier ? `${props.qualifier}-config` : 'aws-accelerator-config'; + const loadAcceleratorConfigTable = new LoadAcceleratorConfigTable(this, 'LoadAcceleratorConfigTable', { + acceleratorConfigTable: configTable, + configRepositoryName: configRepoName, + managementAccountEmail: props.accountsConfig.getManagementAccount().email, + auditAccountEmail: props.accountsConfig.getAuditAccount().email, + logArchiveAccountEmail: props.accountsConfig.getLogArchiveAccount().email, + configS3Bucket: organzationsConfigAsset.s3BucketName, + organizationsConfigS3Key: organzationsConfigAsset.s3ObjectKey, + accountConfigS3Key: accountConfigAsset.s3ObjectKey, + commitId: props.configCommitId || '', + partition: props.partition, + region: cdk.Stack.of(this).region, + managementAccountId: props.accountsConfig.getManagementAccountId(), + stackName: cdk.Stack.of(this).stackName, + kmsKey: key, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }); + + Logger.info(`[prepare-stack] Call create ou construct`); + const createOrganizationalUnits = new OrganizationalUnits(this, 'CreateOrganizationalUnits', { + acceleratorConfigTable: configTable, + commitId: props.configCommitId || '', + controlTowerEnabled: props.globalConfig.controlTower.enable, + organizationsEnabled: props.organizationConfig.enable, + kmsKey: key, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }); + + createOrganizationalUnits.node.addDependency(configTable); + + if (props.partition == 'aws') { + let govCloudAccountMappingTable: cdk.aws_dynamodb.ITable | undefined; + Logger.info(`[prepare-stack] newOrgAccountsTable`); + const newOrgAccountsTable = new cdk.aws_dynamodb.Table(this, 'NewOrgAccounts', { + partitionKey: { name: 'accountEmail', type: cdk.aws_dynamodb.AttributeType.STRING }, + billingMode: cdk.aws_dynamodb.BillingMode.PAY_PER_REQUEST, + encryption: cdk.aws_dynamodb.TableEncryption.CUSTOMER_MANAGED, + encryptionKey: key, + removalPolicy: cdk.RemovalPolicy.DESTROY, + pointInTimeRecovery: true, + }); + + // AwsSolutions-DDB3: The DynamoDB table does not have Point-in-time Recovery enabled. + NagSuppressions.addResourceSuppressionsByPath(this, `${this.stackName}/NewOrgAccounts/Resource`, [ + { + id: 'AwsSolutions-DDB3', + reason: 'NewOrgAccounts DynamoDB table do not need point in time recovery, data can be re-created', + }, + ]); + + Logger.info(`[prepare-stack] newControlTowerAccountsTable`); + const newCTAccountsTable = new cdk.aws_dynamodb.Table(this, 'NewCTAccounts', { + partitionKey: { name: 'accountEmail', type: cdk.aws_dynamodb.AttributeType.STRING }, + billingMode: cdk.aws_dynamodb.BillingMode.PAY_PER_REQUEST, + encryption: cdk.aws_dynamodb.TableEncryption.CUSTOMER_MANAGED, + encryptionKey: key, + removalPolicy: cdk.RemovalPolicy.DESTROY, + pointInTimeRecovery: true, + }); + + // AwsSolutions-DDB3: The DynamoDB table does not have Point-in-time Recovery enabled. + NagSuppressions.addResourceSuppressionsByPath(this, `${this.stackName}/NewCTAccounts/Resource`, [ + { + id: 'AwsSolutions-DDB3', + reason: 'NewCTAccounts DynamoDB table do not need point in time recovery, data can be re-created', + }, + ]); + + new cdk.aws_ssm.StringParameter(this, 'NewCTAccountsTableNameParameter', { + parameterName: `/accelerator/prepare-stack/NewCTAccountsTableName`, + stringValue: newCTAccountsTable.tableName, + }); + + if (props.accountsConfig.anyGovCloudAccounts()) { + Logger.info(`[prepare-stack] Create GovCloudAccountsMappingTable`); + govCloudAccountMappingTable = new cdk.aws_dynamodb.Table(this, 'govCloudAccountMapping', { + partitionKey: { name: 'commercialAccountId', type: cdk.aws_dynamodb.AttributeType.STRING }, + billingMode: cdk.aws_dynamodb.BillingMode.PAY_PER_REQUEST, + encryption: cdk.aws_dynamodb.TableEncryption.CUSTOMER_MANAGED, + encryptionKey: key, + pointInTimeRecovery: true, + }); + + new cdk.aws_ssm.StringParameter(this, 'GovCloudAccountMappingTableNameParameter', { + parameterName: `/accelerator/prepare-stack/govCloudAccountMappingTableName`, + stringValue: govCloudAccountMappingTable.tableName, + }); + } + + new cdk.aws_ssm.StringParameter(this, 'NewOrgAccountsTableNameParameter', { + parameterName: `/accelerator/prepare-stack/NewOrgAccountsTableName`, + stringValue: newOrgAccountsTable.tableName, + }); + + Logger.info(`[prepare-stack] Validate Environment`); + const validation = new ValidateEnvironmentConfig(this, 'ValidateEnvironmentConfig', { + acceleratorConfigTable: configTable, + newOrgAccountsTable: newOrgAccountsTable, + newCTAccountsTable: newCTAccountsTable, + controlTowerEnabled: props.globalConfig.controlTower.enable, + commitId: loadAcceleratorConfigTable.id, + stackName: cdk.Stack.of(this).stackName, + region: cdk.Stack.of(this).region, + managementAccountId: props.accountsConfig.getManagementAccountId(), + partition: props.partition, + kmsKey: key, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }); + + validation.node.addDependency(loadAcceleratorConfigTable); + validation.node.addDependency(createOrganizationalUnits); + + Logger.info(`[prepare-stack] Create new organization accounts`); + const organizationAccounts = new CreateOrganizationAccounts(this, 'CreateOrganizationAccounts', { + newOrgAccountsTable: newOrgAccountsTable, + govCloudAccountMappingTable: govCloudAccountMappingTable, + accountRoleName: props.globalConfig.managementAccountAccessRole, + kmsKey: key, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }); + organizationAccounts.node.addDependency(validation); + + if (props.globalConfig.controlTower.enable) { + Logger.info(`[prepare-stack] Get Portfolio Id`); + const portfolioResults = new GetPortfolioId(this, 'GetPortFolioId', { + displayName: 'AWS Control Tower Account Factory Portfolio', + providerName: 'AWS Control Tower', + kmsKey: key, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }); + Logger.info(`[prepare-stack] Create new control tower accounts`); + const controlTowerAccounts = new CreateControlTowerAccounts(this, 'CreateCTAccounts', { + table: newCTAccountsTable, + portfolioId: portfolioResults.portfolioId, + kmsKey: key, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }); + controlTowerAccounts.node.addDependency(validation); + controlTowerAccounts.node.addDependency(organizationAccounts); + } + } + } + + // AwsSolutions-IAM4: The IAM user, role, or group uses AWS managed policies + NagSuppressions.addResourceSuppressionsByPath( + this, + `${this.stackName}/CreateOrganizationAccounts/CreateOrganizationAccountsProvider/framework-onEvent/ServiceRole/Resource`, + [ + { + id: 'AwsSolutions-IAM4', + reason: 'AWS Custom resource provider framework-onEvent role created by cdk.', + }, + ], + ); + + // AwsSolutions-IAM4: The IAM user, role, or group uses AWS managed policies + NagSuppressions.addResourceSuppressionsByPath( + this, + `${this.stackName}/CreateCTAccounts/CreateControlTowerAcccountsProvider/framework-onTimeout/ServiceRole/Resource`, + [ + { + id: 'AwsSolutions-IAM4', + reason: 'AWS Custom resource provider framework-onTimeout role created by cdk.', + }, + ], + ); + + // AwsSolutions-IAM4: The IAM user, role, or group uses AWS managed policies + NagSuppressions.addResourceSuppressionsByPath( + this, + `${this.stackName}/CreateCTAccounts/CreateControlTowerAcccountsProvider/framework-isComplete/ServiceRole/Resource`, + [ + { + id: 'AwsSolutions-IAM4', + reason: + 'AWS Custom resource provider framework-isComplete role created by cdk. Provisioning products and service catalog needs AWSServiceCatalogEndUserFullAccess managed policy access.', + }, + ], + ); + + // AwsSolutions-IAM4: The IAM user, role, or group uses AWS managed policies + NagSuppressions.addResourceSuppressionsByPath( + this, + `${this.stackName}/CreateCTAccounts/CreateControlTowerAcccountsProvider/framework-onEvent/ServiceRole/Resource`, + [ + { + id: 'AwsSolutions-IAM4', + reason: 'AWS Custom resource provider framework-onEvent role created by cdk.', + }, + ], + ); + + // AwsSolutions-IAM5: The IAM entity contains wildcard permissions and does not have a cdk_nag rule suppression with evidence for those permission + NagSuppressions.addResourceSuppressionsByPath( + this, + `${this.stackName}/CreateCTAccounts/CreateControlTowerAccountStatus/ServiceRole/DefaultPolicy/Resource`, + [ + { + id: 'AwsSolutions-IAM5', + reason: 'AWS Custom resource provider service role created by cdk.', + }, + ], + ); + + // AwsSolutions-IAM4: The IAM user, role, or group uses AWS managed policies + NagSuppressions.addResourceSuppressionsByPath( + this, + `${this.stackName}/CreateCTAccounts/CreateControlTowerAccountStatus/ServiceRole/Resource`, + [ + { + id: 'AwsSolutions-IAM4', + reason: 'AWS Custom resource provider service role created by cdk.', + }, + ], + ); + + // AwsSolutions-IAM4: The IAM user, role, or group uses AWS managed policies + NagSuppressions.addResourceSuppressionsByPath( + this, + `${this.stackName}/CreateCTAccounts/CreateControlTowerAccount/ServiceRole/Resource`, + [ + { + id: 'AwsSolutions-IAM4', + reason: 'AWS Custom resource provider service role created by cdk.', + }, + ], + ); + + // AwsSolutions-IAM4: The IAM user, role, or group uses AWS managed policies + NagSuppressions.addResourceSuppressionsByPath( + this, + `${this.stackName}/CreateOrganizationAccounts/CreateOrganizationAccountsProvider/framework-onTimeout/ServiceRole/Resource`, + [ + { + id: 'AwsSolutions-IAM4', + reason: 'AWS Custom resource provider framework-onTimeout role created by cdk.', + }, + ], + ); + + // AwsSolutions-IAM4: The IAM user, role, or group uses AWS managed policies + NagSuppressions.addResourceSuppressionsByPath( + this, + `${this.stackName}/CreateOrganizationAccounts/CreateOrganizationAccountsProvider/framework-isComplete/ServiceRole/Resource`, + [ + { + id: 'AwsSolutions-IAM4', + reason: 'AWS Custom resource provider framework-isComplete role created by cdk.', + }, + ], + ); + + // AwsSolutions-IAM5: The IAM entity contains wildcard permissions and does not have a cdk_nag rule suppression with evidence for those permission. + NagSuppressions.addResourceSuppressionsByPath( + this, + `${this.stackName}/CreateOrganizationAccounts/CreateOrganizationAccountStatus/ServiceRole/DefaultPolicy/Resource`, + [ + { + id: 'AwsSolutions-IAM5', + reason: 'AWS Custom resource provider service role created by cdk.', + }, + ], + ); + + // AwsSolutions-IAM4: The IAM user, role, or group uses AWS managed policies + NagSuppressions.addResourceSuppressionsByPath( + this, + `${this.stackName}/CreateOrganizationAccounts/CreateOrganizationAccountStatus/ServiceRole/Resource`, + [ + { + id: 'AwsSolutions-IAM4', + reason: 'AWS Custom resource provider service role created by cdk.', + }, + ], + ); + + // AwsSolutions-IAM4: The IAM user, role, or group uses AWS managed policies + NagSuppressions.addResourceSuppressionsByPath( + this, + `${this.stackName}/CreateOrganizationAccounts/CreateOrganizationAccounts/ServiceRole/Resource`, + [ + { + id: 'AwsSolutions-IAM4', + reason: 'AWS Custom resource provider service role created by cdk.', + }, + ], + ); + + Logger.info('[prepare-stack] Completed stack synthesis'); + } +} diff --git a/source/packages/@aws-accelerator/accelerator/lib/stacks/security-audit-stack.ts b/source/packages/@aws-accelerator/accelerator/lib/stacks/security-audit-stack.ts new file mode 100644 index 000000000..26bc89ac9 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/lib/stacks/security-audit-stack.ts @@ -0,0 +1,430 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { NagSuppressions } from 'cdk-nag'; +import { Construct } from 'constructs'; +import * as fs from 'fs'; +import * as yaml from 'js-yaml'; +import { pascalCase } from 'pascal-case'; +import * as path from 'path'; +import { S3ServerAccessLogsBucketNamePrefix } from '../accelerator'; + +import { Region } from '@aws-accelerator/config'; +import { + Bucket, + BucketEncryptionType, + Document, + GuardDutyDetectorConfig, + GuardDutyExportConfigDestinationTypes, + GuardDutyMembers, + KeyLookup, + MacieMembers, + Organization, + SecurityHubMembers, +} from '@aws-accelerator/constructs'; + +import { Logger } from '../logger'; +import { AcceleratorStack, AcceleratorStackProps } from './accelerator-stack'; +import { KeyStack } from './key-stack'; +import { LifecycleRule } from '@aws-accelerator/constructs/lib/aws-s3/bucket'; + +export class SecurityAuditStack extends AcceleratorStack { + constructor(scope: Construct, id: string, props: AcceleratorStackProps) { + super(scope, id, props); + + const key = new KeyLookup(this, 'AcceleratorKeyLookup', { + accountId: props.accountsConfig.getAuditAccountId(), + roleName: KeyStack.CROSS_ACCOUNT_ACCESS_ROLE_NAME, + keyArnParameterName: KeyStack.ACCELERATOR_KEY_ARN_PARAMETER_NAME, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }).getKey(); + + const organizationId = props.organizationConfig.enable ? new Organization(this, 'Organization').id : ''; + + // + // Macie configuration + // + Logger.debug( + `[security-audit-stack] centralSecurityServices.macie.enable: ${props.securityConfig.centralSecurityServices.macie.enable}`, + ); + + if (props.securityConfig.centralSecurityServices.macie.enable) { + Logger.info( + `[security-audit-stack] Creating macie export config bucket - aws-accelerator-securitymacie-${cdk.Aws.ACCOUNT_ID}-${cdk.Aws.REGION}`, + ); + const lifecycleRules: LifecycleRule[] = []; + for (const lifecycleRule of props.securityConfig.centralSecurityServices.macie.lifecycleRules ?? []) { + const noncurrentVersionTransitions = []; + for (const noncurrentVersionTransition of lifecycleRule.noncurrentVersionTransitions) { + noncurrentVersionTransitions.push({ + storageClass: noncurrentVersionTransition.storageClass, + transitionAfter: noncurrentVersionTransition.transitionAfter, + }); + } + const transitions = []; + for (const transition of lifecycleRule.transitions) { + transitions.push({ + storageClass: transition.storageClass, + transitionAfter: transition.transitionAfter, + }); + } + const rule: LifecycleRule = { + abortIncompleteMultipartUploadAfter: lifecycleRule.abortIncompleteMultipartUpload, + enabled: lifecycleRule.enabled, + expiration: lifecycleRule.expiration, + expiredObjectDeleteMarker: lifecycleRule.expiredObjectDeleteMarker, + id: lifecycleRule.id, + noncurrentVersionExpiration: lifecycleRule.noncurrentVersionExpiration, + noncurrentVersionTransitions, + transitions, + }; + lifecycleRules.push(rule); + } + + const S3ServerAccessLogsBucketNamePrefix = 'aws-accelerator-s3-access-logs'; + const bucket = new Bucket(this, 'AwsMacieExportConfigBucket', { + encryptionType: BucketEncryptionType.SSE_KMS, + s3BucketName: `aws-accelerator-org-macie-disc-repo-${cdk.Aws.ACCOUNT_ID}-${cdk.Aws.REGION}`, + kmsKey: key, + serverAccessLogsBucketName: `${S3ServerAccessLogsBucketNamePrefix}-${cdk.Aws.ACCOUNT_ID}-${cdk.Aws.REGION}`, + lifecycleRules, + }); + + new cdk.aws_ssm.StringParameter(this, 'SsmParamOrganizationMacieExportConfigBucketName', { + parameterName: '/accelerator/organization/security/macie/discovery-repository/bucket-name', + stringValue: bucket.getS3Bucket().bucketName, + }); + + // Grant macie access to the bucket + bucket.getS3Bucket().grantReadWrite(new cdk.aws_iam.ServicePrincipal('macie.amazonaws.com')); + + // Grant organization principals to use the bucket + bucket.getS3Bucket().addToResourcePolicy( + new cdk.aws_iam.PolicyStatement({ + sid: 'Allow Organization principals to use of the bucket', + effect: cdk.aws_iam.Effect.ALLOW, + actions: ['s3:GetBucketLocation', 's3:PutObject'], + principals: [new cdk.aws_iam.AnyPrincipal()], + resources: [bucket.getS3Bucket().bucketArn, `${bucket.getS3Bucket().bucketArn}/*`], + conditions: { + StringEquals: { + 'aws:PrincipalOrgID': organizationId, + }, + }, + }), + ); + + // We also tag the bucket to record the fact that it has access for macie principal. + cdk.Tags.of(bucket).add('aws-cdk:auto-macie-access-bucket', 'true'); + + if ( + props.securityConfig.centralSecurityServices.macie.excludeRegions!.indexOf( + cdk.Stack.of(this).region as Region, + ) === -1 + ) { + Logger.info('[security-audit-stack] Adding Macie'); + + new MacieMembers(this, 'MacieMembers', { + adminAccountId: cdk.Stack.of(this).account, + kmsKey: key, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }); + } + } + + // + // GuardDuty configuration + // + Logger.debug( + `[security-audit-stack] centralSecurityServices.guardduty.enable: ${props.securityConfig.centralSecurityServices.guardduty.enable}`, + ); + + if (props.securityConfig.centralSecurityServices.guardduty.enable) { + const lifecycleRules: LifecycleRule[] = []; + for (const lifecycleRule of props.securityConfig.centralSecurityServices.guardduty.lifecycleRules ?? []) { + const noncurrentVersionTransitions = []; + for (const noncurrentVersionTransition of lifecycleRule.noncurrentVersionTransitions) { + noncurrentVersionTransitions.push({ + storageClass: noncurrentVersionTransition.storageClass, + transitionAfter: noncurrentVersionTransition.transitionAfter, + }); + } + const transitions = []; + for (const transition of lifecycleRule.transitions) { + transitions.push({ + storageClass: transition.storageClass, + transitionAfter: transition.transitionAfter, + }); + } + const rule: LifecycleRule = { + abortIncompleteMultipartUploadAfter: lifecycleRule.abortIncompleteMultipartUpload, + enabled: lifecycleRule.enabled, + expiration: lifecycleRule.expiration, + expiredObjectDeleteMarker: lifecycleRule.expiredObjectDeleteMarker, + id: lifecycleRule.id, + noncurrentVersionExpiration: lifecycleRule.noncurrentVersionExpiration, + noncurrentVersionTransitions, + transitions, + }; + lifecycleRules.push(rule); + } + + const bucket = new Bucket(this, 'GuardDutyPublishingDestinationBucket', { + encryptionType: BucketEncryptionType.SSE_KMS, + s3BucketName: `aws-accelerator-org-gduty-pub-dest-${cdk.Aws.ACCOUNT_ID}-${cdk.Aws.REGION}`, + kmsKey: key, + serverAccessLogsBucketName: `${S3ServerAccessLogsBucketNamePrefix}-${cdk.Aws.ACCOUNT_ID}-${cdk.Aws.REGION}`, + lifecycleRules, + }); + + // AwsSolutions-S1: The S3 Bucket has server access logs disabled. + NagSuppressions.addResourceSuppressionsByPath( + this, + `${this.stackName}/GuardDutyPublishingDestinationBucket/Resource/Resource`, + [ + { + id: 'AwsSolutions-S1', + reason: + 'GuardDutyPublishingDestinationBucket has server access logs disabled till the task for access logging completed.', + }, + ], + ); + + new cdk.aws_ssm.StringParameter(this, 'SsmParamOrganizationGuardDutyPublishingDestinationBucketArn', { + parameterName: '/accelerator/organization/security/guardduty/publishing-destination/bucket-arn', + stringValue: bucket.getS3Bucket().bucketArn, + }); + + // Grant guardduty access to the bucket + bucket.getS3Bucket().grantReadWrite(new cdk.aws_iam.ServicePrincipal('guardduty.amazonaws.com')); + + // Grant organization principals to use the bucket + bucket.getS3Bucket().addToResourcePolicy( + new cdk.aws_iam.PolicyStatement({ + sid: 'Allow Organization principals to use of the bucket', + effect: cdk.aws_iam.Effect.ALLOW, + actions: ['s3:GetBucketLocation', 's3:PutObject'], + principals: [new cdk.aws_iam.AnyPrincipal()], + resources: [bucket.getS3Bucket().bucketArn, `${bucket.getS3Bucket().bucketArn}/*`], + conditions: { + StringEquals: { + 'aws:PrincipalOrgID': organizationId, + }, + }, + }), + ); + + // We also tag the bucket to record the fact that it has access for guardduty principal. + cdk.Tags.of(bucket).add('aws-cdk:auto-guardduty-access-bucket', 'true'); + + if ( + props.securityConfig.centralSecurityServices.guardduty.excludeRegions!.indexOf( + cdk.Stack.of(this).region as Region, + ) === -1 + ) { + Logger.info('[security-audit-stack] Adding GuardDuty '); + + const guardDutyMembers = new GuardDutyMembers(this, 'GuardDutyMembers', { + enableS3Protection: props.securityConfig.centralSecurityServices.guardduty.s3Protection.enable, + kmsKey: key, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }); + + new GuardDutyDetectorConfig(this, 'GuardDutyDetectorConfig', { + isExportConfigEnable: + props.securityConfig.centralSecurityServices.guardduty.exportConfiguration.enable && + !props.securityConfig.centralSecurityServices.guardduty.s3Protection.excludeRegions!.includes( + cdk.Stack.of(this).region as Region, + ), + exportDestination: GuardDutyExportConfigDestinationTypes.S3, + exportFrequency: props.securityConfig.centralSecurityServices.guardduty.exportConfiguration.exportFrequency, + kmsKey: key, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }).node.addDependency(guardDutyMembers); + } + } + + // + // SecurityHub configuration + // + Logger.debug( + `[security-audit-stack] centralSecurityServices.securityHub.enable: ${props.securityConfig.centralSecurityServices.securityHub.enable}`, + ); + if ( + props.securityConfig.centralSecurityServices.securityHub.enable && + props.securityConfig.centralSecurityServices.securityHub.excludeRegions!.indexOf( + cdk.Stack.of(this).region as Region, + ) === -1 + ) { + Logger.info('[security-audit-stack] Adding SecurityHub '); + + new SecurityHubMembers(this, 'SecurityHubMembers', { + kmsKey: key, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }); + } + + // + // SSM Automation Docs + // + Logger.info(`[security-audit-stack] Adding SSM Automation Docs`); + if ( + props.securityConfig.centralSecurityServices.ssmAutomation.excludeRegions === undefined || + props.securityConfig.centralSecurityServices.ssmAutomation.excludeRegions.indexOf( + cdk.Stack.of(this).region as Region, + ) === -1 + ) { + // + for (const documentSetItem of props.securityConfig.centralSecurityServices.ssmAutomation.documentSets ?? []) { + // Create list of accounts to share with + const accountIds: string[] = this.getAccountIdsFromShareTarget(documentSetItem.shareTargets); + + for (const documentItem of documentSetItem.documents ?? []) { + Logger.info(`[security-audit-stack] Adding ${documentItem.name}`); + + // Read in the document which should be properly formatted + const buffer = fs.readFileSync(path.join(props.configDirPath, documentItem.template), 'utf8'); + + let content; + if (documentItem.template.endsWith('.json')) { + content = JSON.parse(buffer); + } else { + content = yaml.load(buffer); + } + + // Create the document + new Document(this, pascalCase(documentItem.name), { + name: documentItem.name, + content, + documentType: 'Automation', + sharedWithAccountIds: accountIds, + kmsKey: key, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }); + } + } + } + + // + // IAM Access Analyzer (Does not have a native service enabler) + // + Logger.debug(`[security-audit-stack] accessAnalyzer.enable: ${props.securityConfig.accessAnalyzer.enable}`); + if (props.securityConfig.accessAnalyzer.enable && props.globalConfig.homeRegion === cdk.Stack.of(this).region) { + Logger.info('[security-audit-stack] Adding IAM Access Analyzer '); + new cdk.aws_accessanalyzer.CfnAnalyzer(this, 'AccessAnalyzer', { + type: 'ORGANIZATION', + }); + } + + // + // SNS Notification Topics and Subscriptions + // + Logger.info(`[security-audit-stack] Create SNS Topics and Subscriptions`); + + // // Create CMK for topic + // // TODO: replace this with the single Accelerator key + // const topicCmk = new cdk.aws_kms.Key(this, 'TopicCmk', { + // enableKeyRotation: true, + // description: 'AWS Accelerator SNS Topic CMK', + // }); + // topicCmk.addAlias('accelerator/sns/topic'); + // topicCmk.addToResourcePolicy( + // new cdk.aws_iam.PolicyStatement({ + // sid: 'Allow Organization use of the key', + // actions: [ + // 'kms:Decrypt', + // 'kms:DescribeKey', + // 'kms:Encrypt', + // 'kms:GenerateDataKey', + // 'kms:GenerateDataKeyPair', + // 'kms:GenerateDataKeyPairWithoutPlaintext', + // 'kms:GenerateDataKeyWithoutPlaintext', + // 'kms:ReEncryptFrom', + // 'kms:ReEncryptTo', + // ], + // principals: [new cdk.aws_iam.AnyPrincipal()], + // resources: ['*'], + // conditions: { + // StringEquals: { + // 'aws:PrincipalOrgID': organizationId, + // }, + // }, + // }), + // ); + // topicCmk.addToResourcePolicy( + // new cdk.aws_iam.PolicyStatement({ + // sid: 'Allow AWS Services to encrypt and describe logs', + // actions: [ + // 'kms:Decrypt', + // 'kms:DescribeKey', + // 'kms:Encrypt', + // 'kms:GenerateDataKey', + // 'kms:GenerateDataKeyPair', + // 'kms:GenerateDataKeyPairWithoutPlaintext', + // 'kms:GenerateDataKeyWithoutPlaintext', + // 'kms:ReEncryptFrom', + // 'kms:ReEncryptTo', + // ], + // principals: [ + // new cdk.aws_iam.ServicePrincipal('cloudwatch.amazonaws.com'), + // new cdk.aws_iam.ServicePrincipal('lambda.amazonaws.com'), + // ], + // resources: ['*'], + // }), + // ); + + // Loop through all the subscription entries + for (const snsSubscriptionItem of props.securityConfig.centralSecurityServices.snsSubscriptions ?? []) { + Logger.info(`[security-audit-stack] Create SNS Topic: ${snsSubscriptionItem.level}`); + const topic = new cdk.aws_sns.Topic(this, `${pascalCase(snsSubscriptionItem.level)}SnsTopic`, { + displayName: `AWS Accelerator - ${snsSubscriptionItem.level} Notifications`, + topicName: `aws-accelerator-${snsSubscriptionItem.level}Notifications`, + masterKey: key, + }); + + // Allowing Publish from CloudWatch Service form any account + topic.grantPublish({ + grantPrincipal: new cdk.aws_iam.ServicePrincipal('cloudwatch.amazonaws.com'), + }); + + // Allowing Publish from Lambda Service form any account + topic.grantPublish({ + grantPrincipal: new cdk.aws_iam.ServicePrincipal('lambda.amazonaws.com'), + }); + + // Allowing Publish from Organization + topic.grantPublish({ + grantPrincipal: new cdk.aws_iam.OrganizationPrincipal(organizationId), + }); + + topic.addToResourcePolicy( + new cdk.aws_iam.PolicyStatement({ + sid: 'Allow Organization list topic', + actions: ['sns:ListSubscriptionsByTopic', 'sns:ListTagsForResource', 'sns:GetTopicAttributes'], + principals: [new cdk.aws_iam.AnyPrincipal()], + resources: [topic.topicArn], + conditions: { + StringEquals: { + 'aws:PrincipalOrgID': organizationId, + }, + }, + }), + ); + + Logger.info(`[security-audit-stack] Create SNS Subscription: ${snsSubscriptionItem.email}`); + topic.addSubscription(new cdk.aws_sns_subscriptions.EmailSubscription(snsSubscriptionItem.email)); + } + Logger.info('[security-audit-stack] Completed stack synthesis'); + } +} diff --git a/source/packages/@aws-accelerator/accelerator/lib/stacks/security-resources-stack.ts b/source/packages/@aws-accelerator/accelerator/lib/stacks/security-resources-stack.ts new file mode 100644 index 000000000..52692053c --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/lib/stacks/security-resources-stack.ts @@ -0,0 +1,753 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { NagSuppressions } from 'cdk-nag'; +import * as config from 'aws-cdk-lib/aws-config'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import { pascalCase } from 'change-case'; +import { Construct } from 'constructs'; +import path from 'path'; + +import { KeyLookup, Organization } from '@aws-accelerator/constructs'; + +import { Logger } from '../logger'; +import { AcceleratorStack, AcceleratorStackProps } from './accelerator-stack'; +import { KeyStack } from './key-stack'; + +enum ACCEL_LOOKUP_TYPE { + KMS = 'KMS', + Bucket = 'Bucket', + CUSTOMER_MANAGED_POLICY = 'CustomerManagedPolicy', + INSTANCE_PROFILE = 'InstanceProfile', + ORGANIZATION_ID = 'OrgId', + ACCOUNT_ID = 'AccountId', + REMEDIATION_FUNCTION_NAME = 'RemediationFunctionName', +} + +interface RemediationParameters { + [key: string]: { + StaticValue?: { + Values: string[]; + }; + ResourceValue?: { + Value: 'RESOURCE_ID'; + }; + }; +} + +/** + * Security Stack, configures local account security services + */ +export class SecurityResourcesStack extends AcceleratorStack { + readonly acceleratorKey: cdk.aws_kms.Key; + readonly auditAccountId: string; + readonly logArchiveAccountId: string; + readonly organizationId: string | undefined; + readonly stackProperties: AcceleratorStackProps; + + constructor(scope: Construct, id: string, props: AcceleratorStackProps) { + super(scope, id, props); + + this.stackProperties = props; + this.auditAccountId = props.accountsConfig.getAuditAccountId(); + this.logArchiveAccountId = props.accountsConfig.getLogArchiveAccountId(); + + if (props.organizationConfig.enable) { + this.organizationId = new Organization(this, 'Organization').id; + } + + this.acceleratorKey = new KeyLookup(this, 'AcceleratorKeyLookup', { + accountId: props.accountsConfig.getAuditAccountId(), + roleName: KeyStack.CROSS_ACCOUNT_ACCESS_ROLE_NAME, + keyArnParameterName: KeyStack.ACCELERATOR_KEY_ARN_PARAMETER_NAME, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }).getKey(); + + // AWS Config - Set up recorder and delivery channel, only if Control Tower + // is not being used. Else the Control Tower SCP will block these calls from + // member accounts + // + // If Control Tower is enabled, make sure to set up AWS Config in the + // management account since this is not enabled by default by Control Tower. + // + // An AWS Control Tower preventive guardrail is enforced with AWS + // Organizations using Service Control Policies (SCPs) that disallows + // configuration changes to AWS Config. + // + let configRecorder: config.CfnConfigurationRecorder | undefined = undefined; + if ( + !props.globalConfig.controlTower.enable || + props.accountsConfig.getManagementAccountId() === cdk.Stack.of(this).account + ) { + if (props.securityConfig.awsConfig.enableConfigurationRecorder) { + const configRecorderRole = new iam.Role(this, 'ConfigRecorderRole', { + assumedBy: new iam.ServicePrincipal('config.amazonaws.com'), + managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWS_ConfigRole')], + }); + + // AwsSolutions-IAM4: The IAM user, role, or group uses AWS managed policies + // rule suppression with evidence for this permission. + NagSuppressions.addResourceSuppressionsByPath(this, `${this.stackName}/ConfigRecorderRole/Resource`, [ + { + id: 'AwsSolutions-IAM4', + reason: 'ConfigRecorderRole needs managed policy service-role/AWS_ConfigRole to administer config rules', + }, + ]); + + /** + * As per the documentation, the config role should have + * the s3:PutObject permission to avoid access denied issues + * while AWS config tries to check the s3 bucket (in another account) write permissions + * https://docs.aws.amazon.com/config/latest/developerguide/s3-bucket-policy.html + * + */ + configRecorderRole.addToPrincipalPolicy( + new iam.PolicyStatement({ + actions: ['s3:PutObject'], + resources: ['*'], + }), + ); + + // AwsSolutions-IAM5: The IAM entity contains wildcard permissions and does not have a cdk_nag + // rule suppression with evidence for this permission. + NagSuppressions.addResourceSuppressionsByPath( + this, + `${this.stackName}/ConfigRecorderRole/DefaultPolicy/Resource`, + [ + { + id: 'AwsSolutions-IAM5', + reason: 'ConfigRecorderRole DefaultPolicy is built by cdk.', + }, + ], + ); + + configRecorder = new config.CfnConfigurationRecorder(this, 'ConfigRecorder', { + roleArn: configRecorderRole.roleArn, + recordingGroup: { + allSupported: true, + includeGlobalResourceTypes: true, + }, + }); + } + + if (props.securityConfig.awsConfig.enableDeliveryChannel) { + new config.CfnDeliveryChannel(this, 'ConfigDeliveryChannel', { + s3BucketName: `aws-accelerator-central-logs-${this.logArchiveAccountId}-${props.globalConfig.homeRegion}`, + configSnapshotDeliveryProperties: { + deliveryFrequency: 'One_Hour', + }, + }); + } + } + + // + // Config Rules + // + Logger.info('[security-resources-stack] Evaluating AWS Config rule sets'); + + for (const ruleSet of props.securityConfig.awsConfig.ruleSets) { + if (!this.isIncluded(ruleSet.deploymentTargets)) { + Logger.info('[security-resources-stack] Item excluded'); + continue; + } + + Logger.info( + `[security-resources-stack] Account (${ + cdk.Stack.of(this).account + }) should be included, deploying AWS Config Rules`, + ); + + for (const rule of ruleSet.rules) { + let configRule: config.ManagedRule | config.CustomRule | undefined; + + if (rule.type && rule.type === 'Custom') { + Logger.info(`[security-resources-stack] Creating custom rule ${rule.name}`); + let ruleScope: config.RuleScope | undefined; + + if (rule.customRule.triggeringResources.lookupType == 'ResourceTypes') { + for (const item of rule.customRule.triggeringResources.lookupValue) { + ruleScope = config.RuleScope.fromResources([config.ResourceType.of(item)]); + } + } + + if (rule.customRule.triggeringResources.lookupType == 'ResourceId') { + ruleScope = config.RuleScope.fromResource( + config.ResourceType.of(rule.customRule.triggeringResources.lookupKey), + rule.customRule.triggeringResources.lookupValue[0], + ); + } + + if (rule.customRule.triggeringResources.lookupType == 'Tag') { + ruleScope = config.RuleScope.fromTag( + rule.customRule.triggeringResources.lookupKey, + rule.customRule.triggeringResources.lookupValue[0], + ); + } + + /** + * Lambda function for config custom role + * Single lambda function can not be used for multiple config custom role, there is a pending issue with CDK team on this + * https://github.com/aws/aws-cdk/issues/17582 + */ + const lambdaFunction = new cdk.aws_lambda.Function(this, pascalCase(rule.name) + '-Function', { + runtime: new cdk.aws_lambda.Runtime(rule.customRule.lambda.runtime), + handler: rule.customRule.lambda.handler, + code: cdk.aws_lambda.Code.fromAsset(path.join(props.configDirPath, rule.customRule.lambda.sourceFilePath)), + description: `AWS Config custom rule function used for "${rule.name}" rule`, + }); + + // Configure lambda log file with encryption and log retention + new cdk.aws_logs.LogGroup(this, pascalCase(rule.name) + '-LogGroup', { + logGroupName: `/aws/lambda/${lambdaFunction.functionName}`, + retention: props.globalConfig.cloudwatchLogRetentionInDays, + encryptionKey: this.acceleratorKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + // Read in the policy document which should be properly formatted json + const policyDocument = require(path.join(props.configDirPath, rule.customRule.lambda.rolePolicyFile)); + // Create a statements list using the PolicyStatement factory + const policyStatements: cdk.aws_iam.PolicyStatement[] = []; + for (const statement of policyDocument.Statement) { + policyStatements.push(cdk.aws_iam.PolicyStatement.fromJson(statement)); + } + + // Assign policy to Lambda + lambdaFunction.role?.attachInlinePolicy( + new cdk.aws_iam.Policy(this, pascalCase(rule.name) + '-LambdaRolePolicy', { + statements: [...policyStatements], + }), + ); + + // AwsSolutions-IAM5: The IAM entity contains wildcard permissions and does not have a cdk_nag rule suppression with evidence for those permission. + NagSuppressions.addResourceSuppressionsByPath( + this, + `${this.stackName}/${pascalCase(rule.name)}-LambdaRolePolicy/Resource`, + [ + { + id: 'AwsSolutions-IAM5', + reason: 'AWS Config rule custom lambda role, created by the permission provided in config repository', + }, + ], + ); + + // AwsSolutions-IAM4: The IAM user, role, or group uses AWS managed policies. + // rule suppression with evidence for this permission. + NagSuppressions.addResourceSuppressionsByPath( + this, + `${this.stackName}/${pascalCase(rule.name)}-Function/ServiceRole/Resource`, + [ + { + id: 'AwsSolutions-IAM4', + reason: 'AWS Config custom rule needs managed readonly access policy', + }, + ], + ); + + configRule = new config.CustomRule(this, pascalCase(rule.name), { + configRuleName: rule.name, + lambdaFunction: lambdaFunction, + periodic: rule.customRule.periodic, + inputParameters: this.getRuleParameters(rule.name, rule.inputParameters), + description: rule.description, + maximumExecutionFrequency: + rule.customRule.maximumExecutionFrequency === undefined + ? undefined + : (rule.customRule.maximumExecutionFrequency as cdk.aws_config.MaximumExecutionFrequency), + ruleScope: ruleScope, + configurationChanges: rule.customRule.configurationChanges, + }); + configRule.node.addDependency(lambdaFunction); + } else { + Logger.info(`[security-resources-stack] Creating managed rule ${rule.name}`); + + const resourceTypes: config.ResourceType[] = []; + for (const resourceType of rule.complianceResourceTypes ?? []) { + resourceTypes.push(config.ResourceType.of(resourceType)); + } + + configRule = new config.ManagedRule(this, pascalCase(rule.name), { + configRuleName: rule.name, + description: rule.description, + identifier: rule.identifier ?? rule.name, + inputParameters: this.getRuleParameters(rule.name, rule.inputParameters), + ruleScope: { + resourceTypes, + }, + }); + } + + if (configRule) { + // Create remediation for config rule + if (rule.remediation) { + const remediationRole = this.createRemediationRole( + rule.name, + path.join(props.configDirPath, rule.remediation.rolePolicyFile), + `arn:${cdk.Stack.of(this).partition}:ssm:${cdk.Stack.of(this).region}:${ + rule.remediation.targetAccountName + ? props.accountsConfig.getAccountId(rule.remediation.targetAccountName) + : props.accountsConfig.getAuditAccountId() + }:document/${rule.remediation.targetId}`, + !!rule.remediation.targetDocumentLambda, + ); + + // If remediation document use action as aws:invokeLambdaFunction, create the lambda function + let remediationLambdaFunction: cdk.aws_lambda.Function | undefined; + if (rule.remediation.targetDocumentLambda) { + remediationLambdaFunction = new cdk.aws_lambda.Function( + this, + pascalCase(rule.name) + '-RemediationFunction', + { + role: remediationRole, + runtime: new cdk.aws_lambda.Runtime(rule.remediation.targetDocumentLambda.runtime), + handler: rule.remediation.targetDocumentLambda.handler, + code: cdk.aws_lambda.Code.fromAsset( + path.join(props.configDirPath, rule.remediation.targetDocumentLambda.sourceFilePath), + ), + description: `Function used in ${rule.remediation.targetId} SSM document for "${rule.name}" custom config rule to remediation`, + }, + ); + + // AwsSolutions-IAM4: The IAM user, role, or group uses AWS managed policies. + // rule suppression with evidence for this permission. + NagSuppressions.addResourceSuppressionsByPath( + this, + `${this.stackName}/${pascalCase(rule.name)}-RemediationFunction/ServiceRole/Resource`, + [ + { + id: 'AwsSolutions-IAM4', + reason: 'AWS Config custom rule needs managed readonly access policy', + }, + ], + ); + + // Configure lambda log file with encryption and log retention + new cdk.aws_logs.LogGroup(this, pascalCase(rule.name) + '-RemediationLogGroup', { + logGroupName: `/aws/lambda/${remediationLambdaFunction.functionName}`, + retention: props.globalConfig.cloudwatchLogRetentionInDays, + encryptionKey: this.acceleratorKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + } + + new config.CfnRemediationConfiguration(this, pascalCase(rule.name) + '-Remediation', { + configRuleName: rule.name, + targetId: `arn:${cdk.Stack.of(this).partition}:ssm:${cdk.Stack.of(this).region}:${ + rule.remediation.targetAccountName + ? props.accountsConfig.getAccountId(rule.remediation.targetAccountName) + : props.accountsConfig.getAuditAccountId() + }:document/${rule.remediation.targetId}`, + targetVersion: rule.remediation.targetVersion, + targetType: 'SSM_DOCUMENT', + + automatic: rule.remediation.automatic, + maximumAutomaticAttempts: rule.remediation.maximumAutomaticAttempts, + retryAttemptSeconds: rule.remediation.retryAttemptSeconds, + parameters: this.getRemediationParameters( + rule.name, + rule.remediation.parameters as string[], + [remediationRole.roleArn], + remediationLambdaFunction ? remediationLambdaFunction.functionName : undefined, + ), + }).node.addDependency(configRule); + } else { + Logger.info(`[security-resources-stack] No remediation provided for custom config rule ${rule.name}`); + } + + if (configRecorder) { + configRule.node.addDependency(configRecorder); + } + } + } + } + + // + // CloudWatch Metrics + // + for (const metricSetItem of props.securityConfig.cloudWatch.metricSets ?? []) { + if (!metricSetItem.regions?.includes(cdk.Stack.of(this).region)) { + Logger.info(`[security-resources-stack] Current region not explicity specified for metric item, skip`); + continue; + } + + if (!this.isIncluded(metricSetItem.deploymentTargets)) { + Logger.info(`[security-resources-stack] Item excluded`); + continue; + } + + for (const metricItem of metricSetItem.metrics ?? []) { + Logger.info(`[security-resources-stack] Creating CloudWatch metric filter ${metricItem.filterName}`); + + new cdk.aws_logs.MetricFilter(this, pascalCase(metricItem.filterName), { + logGroup: cdk.aws_logs.LogGroup.fromLogGroupName( + this, + `${pascalCase(metricItem.filterName)}_${pascalCase(metricItem.logGroupName)}`, + metricItem.logGroupName, + ), + metricNamespace: metricItem.metricNamespace, + metricName: metricItem.metricName, + filterPattern: cdk.aws_logs.FilterPattern.literal(metricItem.filterPattern), + metricValue: metricItem.metricValue, + }); + } + } + + // + // CloudWatch Alarms + // + for (const alarmSetItem of props.securityConfig.cloudWatch.alarmSets ?? []) { + if (!alarmSetItem.regions?.includes(cdk.Stack.of(this).region)) { + Logger.info(`[security-resources-stack] Current region not explicity specified for alarm item, skip`); + continue; + } + + if (!this.isIncluded(alarmSetItem.deploymentTargets)) { + Logger.info(`[security-resources-stack] Item excluded`); + continue; + } + + for (const alarmItem of alarmSetItem.alarms ?? []) { + Logger.info(`[security-resources-stack] Creating CloudWatch alarm ${alarmItem.alarmName}`); + + const alarm = new cdk.aws_cloudwatch.Alarm(this, pascalCase(alarmItem.alarmName), { + alarmName: alarmItem.alarmName, + alarmDescription: alarmItem.alarmDescription, + metric: new cdk.aws_cloudwatch.Metric({ + metricName: alarmItem.metricName, + namespace: alarmItem.namespace, + period: cdk.Duration.seconds(alarmItem.period), + statistic: alarmItem.statistic, + }), + comparisonOperator: this.getComparisonOperator(alarmItem.comparisonOperator), + evaluationPeriods: alarmItem.evaluationPeriods, + threshold: alarmItem.threshold, + treatMissingData: this.getTreatMissingData(alarmItem.treatMissingData), + }); + + alarm.addAlarmAction( + new cdk.aws_cloudwatch_actions.SnsAction( + cdk.aws_sns.Topic.fromTopicArn( + this, + `${pascalCase(alarmItem.alarmName)}Topic`, + cdk.Stack.of(this).formatArn({ + service: 'sns', + region: cdk.Stack.of(this).region, + account: props.accountsConfig.getAuditAccountId(), + resource: `aws-accelerator-${alarmItem.snsAlertLevel}Notifications`, + arnFormat: cdk.ArnFormat.NO_RESOURCE_NAME, + }), + ), + ), + ); + } + } + Logger.info('[security-resources-stack] Completed stack synthesis'); + } + + private getComparisonOperator(comparisonOperator: string): cdk.aws_cloudwatch.ComparisonOperator { + if (comparisonOperator === 'GreaterThanOrEqualToThreshold') { + return cdk.aws_cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD; + } + if (comparisonOperator === 'GreaterThanThreshold') { + return cdk.aws_cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD; + } + if (comparisonOperator === 'LessThanThreshold') { + return cdk.aws_cloudwatch.ComparisonOperator.LESS_THAN_THRESHOLD; + } + if (comparisonOperator === 'LessThanOrEqualToThreshold') { + return cdk.aws_cloudwatch.ComparisonOperator.LESS_THAN_OR_EQUAL_TO_THRESHOLD; + } + if (comparisonOperator === 'LessThanLowerOrGreaterThanUpperThreshold') { + return cdk.aws_cloudwatch.ComparisonOperator.LESS_THAN_LOWER_OR_GREATER_THAN_UPPER_THRESHOLD; + } + if (comparisonOperator === 'GreaterThanUpperThreshold') { + return cdk.aws_cloudwatch.ComparisonOperator.GREATER_THAN_UPPER_THRESHOLD; + } + if (comparisonOperator === 'LessThanLowerThreshold') { + return cdk.aws_cloudwatch.ComparisonOperator.LESS_THAN_LOWER_THRESHOLD; + } + return cdk.aws_cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD; + } + + private getTreatMissingData(treatMissingData: string): cdk.aws_cloudwatch.TreatMissingData { + if (treatMissingData === 'breaching') { + return cdk.aws_cloudwatch.TreatMissingData.BREACHING; + } + if (treatMissingData === 'notBreaching') { + return cdk.aws_cloudwatch.TreatMissingData.NOT_BREACHING; + } + if (treatMissingData === 'ignore') { + return cdk.aws_cloudwatch.TreatMissingData.IGNORE; + } + if (treatMissingData === 'missing') { + return cdk.aws_cloudwatch.TreatMissingData.MISSING; + } + return cdk.aws_cloudwatch.TreatMissingData.NOT_BREACHING; + } + + /** + * Function to prepare config rule parameters + * @param ruleName + * @param params + * @private + */ + private getRuleParameters(ruleName: string, params?: { [key: string]: string }): { [key: string]: string } { + if (params) { + const returnParams: { [key: string]: string } = {}; + for (const [key, value] of Object.entries(params)) { + const replacementValues: string[] = []; + for (const item of value.split(',')) { + const parameterReplacementNeeded = (item as string).match('\\${ACCEL_LOOKUP::([a-zA-Z0-9-/:]*)}'); + if (parameterReplacementNeeded) { + const replacementValue = this.getReplacementValue(ruleName, parameterReplacementNeeded, 'Rule-Parameter'); + replacementValues.push(replacementValue?.split(',')[0] ?? ''); + } + } + + if (replacementValues.length > 0) { + returnParams[key] = replacementValues.join(','); + } else { + returnParams[key] = value; + } + } + return returnParams; + } else { + return {}; + } + } + + /** + * Function to get remediation parameters + * @param ruleName + * @param params + * @param assumeRoleArn + * @param configFunctionName + * @private + */ + private getRemediationParameters( + ruleName: string, + params?: string[], + assumeRoleArn?: string[], + configFunctionName?: string, + ): RemediationParameters | undefined { + if (!params) { + return undefined; + } + + const returnParams: RemediationParameters = {}; + if (assumeRoleArn) { + returnParams['AutomationAssumeRole'] = { + StaticValue: { + Values: assumeRoleArn, + }, + }; + } + + for (const param of params) { + let parameterName: string | undefined; + let parameterValue: string | undefined; + let parameterType = 'List'; + for (const [key, value] of Object.entries(param)) { + if (key === 'name') { + parameterName = value; + } + if (key === 'value') { + parameterValue = value as string; + } + if (key === 'type') { + parameterType = value; + } + } + + const replacementValues: string[] = []; + for (const item of (parameterValue as string).split(',')) { + const parameterReplacementNeeded = (item as string).match('\\${ACCEL_LOOKUP::([a-zA-Z0-9-/:]*)}'); + if (parameterReplacementNeeded) { + const replacementValue = this.getReplacementValue( + ruleName, + parameterReplacementNeeded, + 'Remediation-Parameter', + configFunctionName, + ); + replacementValues.push(replacementValue ?? ''); + } + } + + if (replacementValues.length > 0) { + if (parameterType === 'StringList') { + returnParams[parameterName!] = { + StaticValue: { + Values: replacementValues, + }, + }; + } + + if (parameterType === 'String') { + returnParams[parameterName!] = { + StaticValue: { + Values: [replacementValues.join(',')], + }, + }; + } + } else { + if (parameterValue === 'RESOURCE_ID') { + returnParams[parameterName!] = { + ResourceValue: { + Value: 'RESOURCE_ID', + }, + }; + } else { + returnParams[parameterName!] = { + StaticValue: { + Values: parameterValue!.split(','), + }, + }; + } + } + } + return returnParams; + } + + /** + * Function to get Config rule remediation parameter replacement value + * @param ruleName + * @param replacement + * @param replacementType + * @param remediationFunctionName + * @private + */ + private getReplacementValue( + ruleName: string, + replacement: RegExpMatchArray, + replacementType: string, + remediationFunctionName?: string, + ): string | undefined { + const replacementArray = replacement[1].split(':'); + const lookupType = replacementArray[0]; + + if (lookupType === ACCEL_LOOKUP_TYPE.REMEDIATION_FUNCTION_NAME && replacementArray.length === 1) { + if (remediationFunctionName) { + return remediationFunctionName; + } else { + throw new Error( + `Remediation function for ${ruleName} rule is undefined. Invalid lookup value ${replacementArray[1]}`, + ); + } + } + + if (lookupType === ACCEL_LOOKUP_TYPE.ORGANIZATION_ID && replacementArray.length === 1) { + if (this.organizationId) { + return this.organizationId; + } else { + throw new Error(`${ruleName} parameter error !! Organization not enabled can not retrieve organization id`); + } + } + + if (lookupType === ACCEL_LOOKUP_TYPE.ACCOUNT_ID) { + return this.stackProperties.accountsConfig.getAccountId(replacementArray[1]); + } + + if (lookupType === ACCEL_LOOKUP_TYPE.INSTANCE_PROFILE && replacementArray.length === 2) { + return replacementArray[1]; + } + + if (lookupType === ACCEL_LOOKUP_TYPE.CUSTOMER_MANAGED_POLICY && replacementArray.length === 2) { + return cdk.aws_iam.ManagedPolicy.fromManagedPolicyName( + this, + `${pascalCase(ruleName)} + ${pascalCase(replacementArray[1])}-${pascalCase(replacementType)}`, + replacementArray[1], + ).managedPolicyArn; + } + + if (lookupType === ACCEL_LOOKUP_TYPE.KMS && replacementArray.length === 1) { + return this.acceleratorKey.keyArn; + } + + if (lookupType === ACCEL_LOOKUP_TYPE.Bucket && replacementArray.length === 2) { + if (replacementArray[1].toLowerCase() === 'elbLogs'.toLowerCase()) { + return `aws-accelerator-elb-access-logs-${this.logArchiveAccountId}-${cdk.Stack.of(this).region}`; + } else { + return cdk.aws_s3.Bucket.fromBucketName( + this, + `${pascalCase(ruleName)}-${pascalCase(replacementType)}-InputBucket`, + replacementArray[1].toLowerCase(), + ).bucketName; + } + } + throw new Error(`Config rule replacement key ${replacement.input} not found`); + } + /** + * Function to create remediation role + * @param ruleName + * @param policyFilePath + * @param resources + * @param isLambdaRole + * @private + */ + private createRemediationRole( + ruleName: string, + policyFilePath: string, + resources?: string, + isLambdaRole = false, + ): cdk.aws_iam.IRole { + // Read in the policy document which should be properly formatted json + const policyDocument = require(policyFilePath); + // Create a statements list using the PolicyStatement factory + const policyStatements: cdk.aws_iam.PolicyStatement[] = []; + for (const statement of policyDocument.Statement) { + policyStatements.push(cdk.aws_iam.PolicyStatement.fromJson(statement)); + } + + const principals: cdk.aws_iam.PrincipalBase[] = [new iam.ServicePrincipal('ssm.amazonaws.com')]; + if (isLambdaRole) { + principals.push(new iam.ServicePrincipal('lambda.amazonaws.com')); + } + + const role = new iam.Role(this, pascalCase(ruleName) + '-RemediationRole', { + assumedBy: new cdk.aws_iam.CompositePrincipal(...principals), + }); + + role.addToPolicy( + new cdk.aws_iam.PolicyStatement({ + effect: cdk.aws_iam.Effect.ALLOW, + actions: [ + 'ssm:GetAutomationExecution', + 'ssm:StartAutomationExecution', + 'ssm:GetParameters', + 'ssm:GetParameter', + 'ssm:PutParameter', + ], + resources: [resources ?? '*'], + }), + ); + + policyStatements.forEach(policyStatement => { + role.addToPolicy(policyStatement); + }); + + // AwsSolutions-IAM5: The IAM entity contains wildcard permissions and does not have a cdk_nag rule suppression with evidence for those permission. + // rule suppression with evidence for this permission. + NagSuppressions.addResourceSuppressionsByPath( + this, + `${this.stackName}/${pascalCase(ruleName)}-RemediationRole/DefaultPolicy/Resource`, + [ + { + id: 'AwsSolutions-IAM5', + reason: 'AWS Config rule remediation role, created by the permission provided in config repository', + }, + ], + ); + return role; + } +} diff --git a/source/packages/@aws-accelerator/accelerator/lib/stacks/security-stack.ts b/source/packages/@aws-accelerator/accelerator/lib/stacks/security-stack.ts new file mode 100644 index 000000000..cb31c8b7f --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/lib/stacks/security-stack.ts @@ -0,0 +1,194 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import { Construct } from 'constructs'; + +import { Region } from '@aws-accelerator/config'; +import { + EbsDefaultEncryption, + GuardDutyPublishingDestination, + KeyLookup, + MacieExportConfigClassification, + PasswordPolicy, + SecurityHubStandards, + // SsmParameterLookup, +} from '@aws-accelerator/constructs'; + +import { Logger } from '../logger'; +import { AcceleratorStack, AcceleratorStackProps } from './accelerator-stack'; +import { KeyStack } from './key-stack'; + +/** + * Security Stack, configures local account security services + */ +export class SecurityStack extends AcceleratorStack { + readonly acceleratorKey: cdk.aws_kms.Key; + readonly auditAccountId: string; + readonly logArchiveAccountId: string; + + constructor(scope: Construct, id: string, props: AcceleratorStackProps) { + super(scope, id, props); + + const auditAccountName = props.securityConfig.getDelegatedAccountName(); + this.auditAccountId = props.accountsConfig.getAuditAccountId(); + this.logArchiveAccountId = props.accountsConfig.getLogArchiveAccountId(); + + this.acceleratorKey = new KeyLookup(this, 'AcceleratorKeyLookup', { + accountId: props.accountsConfig.getAuditAccountId(), + roleName: KeyStack.CROSS_ACCOUNT_ACCESS_ROLE_NAME, + keyArnParameterName: KeyStack.ACCELERATOR_KEY_ARN_PARAMETER_NAME, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }).getKey(); + + // + // MacieSession configuration + // + if ( + props.securityConfig.centralSecurityServices.macie.enable && + props.securityConfig.centralSecurityServices.macie.excludeRegions!.indexOf( + cdk.Stack.of(this).region as Region, + ) === -1 + ) { + if (props.accountsConfig.containsAccount(auditAccountName)) { + const bucketName = `aws-accelerator-org-macie-disc-repo-${this.auditAccountId}-${cdk.Aws.REGION}`; + + new MacieExportConfigClassification(this, 'AwsMacieUpdateExportConfigClassification', { + bucketName: bucketName, + kmsKey: this.acceleratorKey, + keyPrefix: `${cdk.Stack.of(this).account}-aws-macie-export-config`, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }); + } else { + throw new Error(`Macie audit delegated admin account name "${auditAccountName}" not found.`); + } + } + + // + // GuardDuty configuration + // + if ( + props.securityConfig.centralSecurityServices.guardduty.enable && + props.securityConfig.centralSecurityServices.guardduty.excludeRegions!.indexOf( + cdk.Stack.of(this).region as Region, + ) === -1 + ) { + if (props.accountsConfig.containsAccount(auditAccountName)) { + const bucketArn = `arn:${cdk.Stack.of(this).partition}:s3:::aws-accelerator-org-gduty-pub-dest-${ + this.auditAccountId + }-${cdk.Stack.of(this).region}`; + + new GuardDutyPublishingDestination(this, 'GuardDutyPublishingDestination', { + exportDestinationType: + props.securityConfig.centralSecurityServices.guardduty.exportConfiguration.destinationType, + bucketArn: bucketArn, + kmsKey: this.acceleratorKey, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }); + } else { + throw new Error(`Guardduty audit delegated admin account name "${auditAccountName}" not found.`); + } + } + + // + // SecurityHub configuration + // + if ( + props.securityConfig.centralSecurityServices.securityHub.enable && + props.securityConfig.centralSecurityServices.securityHub.excludeRegions!.indexOf( + cdk.Stack.of(this).region as Region, + ) === -1 + ) { + if (props.accountsConfig.containsAccount(auditAccountName)) { + new SecurityHubStandards(this, 'SecurityHubStandards', { + standards: props.securityConfig.centralSecurityServices.securityHub.standards, + kmsKey: this.acceleratorKey, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }); + } else { + throw new Error(`SecurityHub audit delegated admin account name "${auditAccountName}" not found.`); + } + } + + // + // Ebs Default Volume Encryption configuration + // + if ( + props.securityConfig.centralSecurityServices.ebsDefaultVolumeEncryption.enable && + props.securityConfig.centralSecurityServices.ebsDefaultVolumeEncryption.excludeRegions!.indexOf( + cdk.Stack.of(this).region as Region, + ) === -1 + ) { + const ebsEncryptionKey = new cdk.aws_kms.Key(this, 'EbsEncryptionKey', { + enableKeyRotation: true, + description: 'EBS Volume Encryption', + }); + ebsEncryptionKey.addToResourcePolicy( + new iam.PolicyStatement({ + sid: 'Allow service-linked role use', + effect: cdk.aws_iam.Effect.ALLOW, + actions: ['kms:Decrypt', 'kms:DescribeKey', 'kms:Encrypt', 'kms:GenerateDataKey*', 'kms:ReEncrypt*'], + principals: [ + new cdk.aws_iam.ArnPrincipal( + `arn:${cdk.Stack.of(this).partition}:iam::${ + cdk.Stack.of(this).account + }:role/aws-service-role/autoscaling.amazonaws.com/AWSServiceRoleForAutoScaling`, + ), + ], + resources: ['*'], + }), + ); + ebsEncryptionKey.addToResourcePolicy( + new iam.PolicyStatement({ + sid: 'Allow Autoscaling to create grant', + effect: cdk.aws_iam.Effect.ALLOW, + actions: ['kms:CreateGrant'], + principals: [ + new cdk.aws_iam.ArnPrincipal( + `arn:${cdk.Stack.of(this).partition}:iam::${ + cdk.Stack.of(this).account + }:role/aws-service-role/autoscaling.amazonaws.com/AWSServiceRoleForAutoScaling`, + ), + ], + resources: ['*'], + conditions: { Bool: { 'kms:GrantIsForAWSResource': 'true' } }, + }), + ); + new EbsDefaultEncryption(this, 'EbsDefaultVolumeEncryption', { + ebsEncryptionKmsKey: ebsEncryptionKey, + logGroupKmsKey: this.acceleratorKey, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }); + + new cdk.aws_ssm.StringParameter(this, 'EbsDefaultVolumeEncryptionParameter', { + parameterName: `/accelerator/security-stack/ebsDefaultVolumeEncryptionKeyArn`, + stringValue: ebsEncryptionKey.keyArn, + }); + } + + // + // Update IAM Password Policy + // + if (props.globalConfig.homeRegion === cdk.Stack.of(this).region) { + Logger.info(`[security-stack] Setting the IAM Password policy`); + new PasswordPolicy(this, 'IamPasswordPolicy', { + ...props.securityConfig.iamPasswordPolicy, + kmsKey: this.acceleratorKey, + logRetentionInDays: props.globalConfig.cloudwatchLogRetentionInDays, + }); + } + + Logger.info('[security-stack] Completed stack synthesis'); + } +} diff --git a/source/packages/@aws-accelerator/accelerator/lib/stacks/tester-pipeline-stack.ts b/source/packages/@aws-accelerator/accelerator/lib/stacks/tester-pipeline-stack.ts new file mode 100644 index 000000000..8e7a4bd86 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/lib/stacks/tester-pipeline-stack.ts @@ -0,0 +1,110 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +import * as cdk from 'aws-cdk-lib'; +import { NagSuppressions } from 'cdk-nag'; +import { Construct } from 'constructs'; +import { TesterPipeline } from '../tester-pipeline'; + +/** + * TesterPipelineStackProps + */ +export interface TesterPipelineStackProps extends cdk.StackProps { + readonly sourceRepositoryName: string; + readonly sourceBranchName: string; + readonly managementCrossAccountRoleName: string; + readonly qualifier?: string; + readonly managementAccountId?: string; + readonly managementAccountRoleName?: string; +} + +/** + * TesterPipelineStack class + */ +export class TesterPipelineStack extends cdk.Stack { + constructor(scope: Construct, id: string, props: TesterPipelineStackProps) { + super(scope, id, props); + + new TesterPipeline(this, 'TesterPipeline', { + sourceRepositoryName: props.sourceRepositoryName, + sourceBranchName: props.sourceBranchName, + managementCrossAccountRoleName: props.managementCrossAccountRoleName, + qualifier: props.qualifier, + managementAccountId: props.managementAccountId, + managementAccountRoleName: props.managementAccountRoleName, + }); + + // AwsSolutions-IAM5: The IAM entity contains wildcard permissions and does not have a cdk_nag rule suppression with evidence for those permission. + NagSuppressions.addResourceSuppressionsByPath( + this, + `${this.stackName}/TesterPipeline/PipelineRole/DefaultPolicy/Resource`, + [ + { + id: 'AwsSolutions-IAM5', + reason: 'PipelineRole DefaultPolicy is built by cdk.', + }, + ], + ); + + // AwsSolutions-IAM5: The IAM entity contains wildcard permissions and does not have a cdk_nag rule suppression with evidence for those permission. + NagSuppressions.addResourceSuppressionsByPath( + this, + `${this.stackName}/TesterPipeline/Resource/Source/Source/CodePipelineActionRole/DefaultPolicy/Resource`, + [ + { + id: 'AwsSolutions-IAM5', + reason: 'Source code pipeline action DefaultPolicy is built by cdk.', + }, + ], + ); + + // AwsSolutions-IAM5: The IAM entity contains wildcard permissions and does not have a cdk_nag rule suppression with evidence for those permission. + NagSuppressions.addResourceSuppressionsByPath( + this, + `${this.stackName}/TesterPipeline/Resource/Source/Configuration/CodePipelineActionRole/DefaultPolicy/Resource`, + [ + { + id: 'AwsSolutions-IAM5', + reason: 'Configuration source pipeline action DefaultPolicy is built by cdk.', + }, + ], + ); + + // AwsSolutions-IAM4: The IAM user, role, or group uses AWS managed policies. + NagSuppressions.addResourceSuppressionsByPath(this, `${this.stackName}/TesterPipeline/DeployRole/Resource`, [ + { + id: 'AwsSolutions-IAM4', + reason: 'Pipeline deploy project role is built by cdk.', + }, + ]); + + // AwsSolutions-IAM5: The IAM entity contains wildcard permissions and does not have a cdk_nag rule suppression with evidence for those permission + NagSuppressions.addResourceSuppressionsByPath( + this, + `${this.stackName}/TesterPipeline/DeployRole/DefaultPolicy/Resource`, + [ + { + id: 'AwsSolutions-IAM5', + reason: 'Pipeline deploy project role is built by cdk.', + }, + ], + ); + + // AwsSolutions-CB3: The CodeBuild project has privileged mode enabled. + NagSuppressions.addResourceSuppressionsByPath(this, `${this.stackName}/TesterPipeline/TesterProject/Resource`, [ + { + id: 'AwsSolutions-CB3', + reason: 'Pipeline tester project allow access to the Docker daemon.', + }, + ]); + } +} diff --git a/source/packages/@aws-accelerator/accelerator/lib/tester-pipeline.ts b/source/packages/@aws-accelerator/accelerator/lib/tester-pipeline.ts new file mode 100644 index 000000000..d6cce41d7 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/lib/tester-pipeline.ts @@ -0,0 +1,225 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import * as codebuild from 'aws-cdk-lib/aws-codebuild'; +import * as codecommit from 'aws-cdk-lib/aws-codecommit'; +import * as codepipeline from 'aws-cdk-lib/aws-codepipeline'; +import * as codepipeline_actions from 'aws-cdk-lib/aws-codepipeline-actions'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as s3_assets from 'aws-cdk-lib/aws-s3-assets'; +import { Construct } from 'constructs'; +import fs from 'fs'; +import * as yaml from 'js-yaml'; +import os from 'os'; +import path from 'path'; + +import { Bucket, BucketEncryptionType } from '@aws-accelerator/constructs'; +import * as cdk_extensions from '@aws-cdk-extensions/cdk-extensions'; + +/** + * TesterPipelineProps + */ +export interface TesterPipelineProps { + readonly sourceRepositoryName: string; + readonly sourceBranchName: string; + readonly managementCrossAccountRoleName: string; + readonly qualifier?: string; + readonly managementAccountId?: string; + readonly managementAccountRoleName?: string; +} + +/** + * AWS Accelerator Functional Test Pipeline Class, which creates the pipeline for Accelerator test + */ +export class TesterPipeline extends Construct { + private readonly pipelineRole: iam.Role; + private readonly deployOutput: codepipeline.Artifact; + private readonly acceleratorRepoArtifact: codepipeline.Artifact; + private readonly configRepoArtifact: codepipeline.Artifact; + + constructor(scope: Construct, id: string, props: TesterPipelineProps) { + super(scope, id); + + let targetAcceleratorEnvVariables: { [p: string]: codebuild.BuildEnvironmentVariable } | undefined; + + const tempDirPath = fs.mkdtempSync(path.join(os.tmpdir(), 'test-config-assets-')); + fs.writeFileSync(path.join(tempDirPath, 'config.yaml'), yaml.dump({ tests: [] }), 'utf8'); + + const configurationDefaultAssets = new s3_assets.Asset(this, 'ConfigurationDefaultAssets', { + path: tempDirPath, + }); + + const configRepository = new cdk_extensions.Repository(this, 'ConfigRepository', { + repositoryName: `${props.qualifier ?? 'aws-accelerator'}-test-config`, + repositoryBranchName: 'main', + s3BucketName: configurationDefaultAssets.bucket.bucketName, + s3key: configurationDefaultAssets.s3ObjectKey, + description: 'AWS Accelerator functional test configuration repository', + }); + + const cfnRepository = configRepository.node.defaultChild as codecommit.CfnRepository; + cfnRepository.applyRemovalPolicy(cdk.RemovalPolicy.RETAIN, { applyToUpdateReplacePolicy: true }); + + if (props.managementAccountId && props.managementAccountRoleName) { + targetAcceleratorEnvVariables = { + MANAGEMENT_ACCOUNT_ID: { + type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: props.managementAccountId, + }, + MANAGEMENT_ACCOUNT_ROLE_NAME: { + type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: props.managementAccountRoleName, + }, + }; + } + + // Get installer key + const installerKey = cdk.aws_kms.Key.fromKeyArn( + this, + 'AcceleratorKey', + cdk.aws_ssm.StringParameter.valueForStringParameter( + this, + props.qualifier + ? `/accelerator/${props.qualifier}/installer/kms/key-arn` + : '/accelerator/installer/kms/key-arn', + ), + ) as cdk.aws_kms.Key; + + const bucket = new Bucket(this, 'SecureBucket', { + encryptionType: BucketEncryptionType.SSE_KMS, + s3BucketName: `${props.qualifier ?? 'aws-accelerator'}-tester-${cdk.Stack.of(this).account}-${ + cdk.Stack.of(this).region + }`, + kmsKey: installerKey, + serverAccessLogsBucketName: cdk.aws_ssm.StringParameter.valueForStringParameter( + this, + props.qualifier + ? `/accelerator/${props.qualifier}/installer-access-logs-bucket-name` + : '/accelerator/installer-access-logs-bucket-name', + ), + }); + + /** + * Functional test pipeline role + */ + this.pipelineRole = new iam.Role(this, 'PipelineRole', { + assumedBy: new iam.ServicePrincipal('codepipeline.amazonaws.com'), + }); + + /** + * Functional test pipeline + */ + const pipeline = new codepipeline.Pipeline(this, 'Resource', { + pipelineName: props.qualifier ? `${props.qualifier}-tester-pipeline` : 'AWSAccelerator-TesterPipeline', + artifactBucket: bucket.getS3Bucket(), + role: this.pipelineRole, + }); + + this.configRepoArtifact = new codepipeline.Artifact('Config'); + this.acceleratorRepoArtifact = new codepipeline.Artifact('Source'); + + pipeline.addStage({ + stageName: 'Source', + actions: [ + new codepipeline_actions.CodeCommitSourceAction({ + actionName: 'Source', + repository: codecommit.Repository.fromRepositoryName(this, 'SourceRepo', props.sourceRepositoryName), + branch: props.sourceBranchName, + output: this.acceleratorRepoArtifact, + trigger: codepipeline_actions.CodeCommitTrigger.NONE, + }), + new codepipeline_actions.CodeCommitSourceAction({ + actionName: 'Configuration', + repository: configRepository, + branch: 'main', + output: this.configRepoArtifact, + trigger: codepipeline_actions.CodeCommitTrigger.EVENTS, + }), + ], + }); + + /** + * Deploy Stage + */ + const deployRole = new iam.Role(this, 'DeployAdminRole', { + assumedBy: new iam.ServicePrincipal('codebuild.amazonaws.com'), + //TODO restricted access + managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('AdministratorAccess')], + }); + + const testerProject = new codebuild.PipelineProject(this, 'TesterProject', { + projectName: props.qualifier ? `${props.qualifier}-tester-project` : 'AWSAccelerator-TesterProject', + encryptionKey: installerKey, + role: deployRole, + buildSpec: codebuild.BuildSpec.fromObjectToYaml({ + version: '0.2', + phases: { + install: { + 'runtime-versions': { + nodejs: 14, + }, + }, + build: { + commands: [ + 'cd source', + 'yarn install', + 'yarn lerna link', + 'yarn build', + 'cd packages/@aws-accelerator/tester', + 'env', + `if [ ! -z "$MANAGEMENT_ACCOUNT_ID" ] && [ ! -z "$MANAGEMENT_ACCOUNT_ROLE_NAME" ]; then yarn run cdk deploy --require-approval never --context account=${cdk.Aws.ACCOUNT_ID} --context region=${cdk.Aws.REGION} --context management-cross-account-role-name=${props.managementCrossAccountRoleName} --context qualifier=${props.qualifier} --context config-dir=$CODEBUILD_SRC_DIR_Config --context management-account-id=${props.managementAccountId} --context management-account-role-name=${props.managementAccountRoleName}; else yarn run cdk deploy --require-approval never --context account=${cdk.Aws.ACCOUNT_ID} --context region=${cdk.Aws.REGION} --context management-cross-account-role-name=${props.managementCrossAccountRoleName} --context config-dir=$CODEBUILD_SRC_DIR_Config; fi`, + ], + }, + }, + }), + environment: { + buildImage: codebuild.LinuxBuildImage.STANDARD_5_0, + privileged: true, // Allow access to the Docker daemon + computeType: codebuild.ComputeType.MEDIUM, + environmentVariables: { + NODE_OPTIONS: { + type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: '--max_old_space_size=4096', + }, + ACCELERATOR_REPOSITORY_NAME: { + type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: props.sourceRepositoryName, + }, + ACCELERATOR_REPOSITORY_BRANCH_NAME: { + type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: props.sourceBranchName, + }, + ...targetAcceleratorEnvVariables, + }, + }, + cache: codebuild.Cache.local(codebuild.LocalCacheMode.SOURCE), + }); + + this.deployOutput = new codepipeline.Artifact('DeployOutput'); + + pipeline.addStage({ + stageName: 'Deploy', + actions: [ + new codepipeline_actions.CodeBuildAction({ + actionName: 'Deploy', + project: testerProject, + input: this.acceleratorRepoArtifact, + extraInputs: [this.configRepoArtifact], + outputs: [this.deployOutput], + role: this.pipelineRole, + }), + ], + }); + } +} diff --git a/source/packages/@aws-accelerator/accelerator/lib/toolkit.ts b/source/packages/@aws-accelerator/accelerator/lib/toolkit.ts new file mode 100644 index 000000000..f8659110d --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/lib/toolkit.ts @@ -0,0 +1,220 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { SdkProvider } from 'aws-cdk/lib/api/aws-auth'; +import { Bootstrapper, BootstrapSource } from 'aws-cdk/lib/api/bootstrap'; +import { CloudFormationDeployments } from 'aws-cdk/lib/api/cloudformation-deployments'; +import { StackSelector } from 'aws-cdk/lib/api/cxapp/cloud-assembly'; +import { CloudExecutable } from 'aws-cdk/lib/api/cxapp/cloud-executable'; +import { execProgram } from 'aws-cdk/lib/api/cxapp/exec'; +import { ToolkitInfo } from 'aws-cdk/lib/api/toolkit-info'; +import { CdkToolkit } from 'aws-cdk/lib/cdk-toolkit'; +import { RequireApproval } from 'aws-cdk/lib/diff'; +import { Command, Configuration } from 'aws-cdk/lib/settings'; + +import { AcceleratorStackNames } from './accelerator'; +import { AcceleratorStage } from './accelerator-stage'; +import { Logger } from './logger'; + +/** + * + */ +export enum AcceleratorToolkitCommand { + BOOTSTRAP = Command.BOOTSTRAP, + DEPLOY = Command.DEPLOY, + DIFF = Command.DIFF, + SYNTH = Command.SYNTH, + SYNTHESIZE = Command.SYNTHESIZE, +} + +/** + * Wrapper around the CdkToolkit. The Accelerator defines this wrapper to add + * the following functionality: + * + * - x + * - y + * - z + */ +export class AcceleratorToolkit { + /** + * + * @returns + */ + static isSupportedCommand(command: string): boolean { + if (command === undefined) { + return false; + } + return Object.values(AcceleratorToolkitCommand).includes(command); + } + + /** + * Accelerator customized execution of the CDKToolkit based on + * aws-cdk/packages/aws-cdk/bin/cdk.ts + * + * + * @param options + */ + static async execute(options: { + command: string; + accountId?: string; + region?: string; + partition: string; + stage?: string; + configDirPath?: string; + requireApproval?: RequireApproval; + trustedAccountId?: string; + app?: string; + }): Promise { + // Logger + if (options.accountId || options.region) { + if (options.stage) { + Logger.info( + `[toolkit] Executing cdk ${options.command} ${options.stage} for aws://${options.accountId}/${options.region}`, + ); + } else { + Logger.info(`[toolkit] Executing cdk ${options.command} for aws://${options.accountId}/${options.region}`); + } + } else if (options.stage) { + Logger.info(`[toolkit] Executing cdk ${options.command} ${options.stage}`); + } else { + Logger.info(`[toolkit] Executing cdk ${options.command}`); + } + + // build the context + const context: string[] = []; + if (options.configDirPath) { + context.push(`config-dir=${options.configDirPath}`); + } + if (options.stage) { + context.push(`stage=${options.stage}`); + } + if (options.accountId) { + context.push(`account=${options.accountId}`); + } + if (options.region) { + context.push(`region=${options.region}`); + } + if (options.partition) { + context.push(`partition=${options.partition}`); + } + + const configuration = new Configuration({ + commandLineArguments: { + _: [options.command as Command, ...[]], + versionReporting: false, + pathMetadata: false, + output: 'cdk.out', + assetMetadata: false, + staging: false, + lookups: false, + app: options.app, + context, + }, + }); + await configuration.load(); + + const sdkProvider = await SdkProvider.withAwsCliCompatibleDefaults({ + profile: configuration.settings.get(['profile']), + }); + + const cloudFormation = new CloudFormationDeployments({ sdkProvider }); + + const cloudExecutable = new CloudExecutable({ + configuration, + sdkProvider, + synthesizer: execProgram, + }); + + const toolkitStackName: string = ToolkitInfo.determineName('AWSAccelerator-CDKToolkit'); + + const cli = new CdkToolkit({ + cloudExecutable, + cloudFormation, + configuration, + sdkProvider, + }); + + switch (options.command) { + case Command.BOOTSTRAP: + const source: BootstrapSource = { source: 'default' }; + const bootstrapper = new Bootstrapper(source); + const environments = [`aws://${options.accountId}/${options.region}`]; + const trustedAccounts: string[] = []; + if (options.trustedAccountId && options.trustedAccountId != options.accountId) { + trustedAccounts.push(options.trustedAccountId); + } + await cli.bootstrap(environments, bootstrapper, { + toolkitStackName, + parameters: { + bucketName: configuration.settings.get(['toolkitBucket', 'bucketName']), + kmsKeyId: configuration.settings.get(['toolkitBucket', 'kmsKeyId']), + qualifier: 'accel', + trustedAccounts, + cloudFormationExecutionPolicies: [`arn:${options.partition}:iam::aws:policy/AdministratorAccess`], + }, + }); + break; + case Command.DIFF: + await cli.diff({ stackNames: [] }); + break; + + case Command.DEPLOY: + if (options.stage === undefined) { + throw new Error('trying to deploy with an undefined stage'); + } + + let stackName = [`${AcceleratorStackNames[options.stage]}-${options.accountId}-${options.region}`]; + + if (options.stage === AcceleratorStage.PIPELINE) { + stackName = process.env['ACCELERATOR_QUALIFIER'] + ? [ + `${process.env['ACCELERATOR_QUALIFIER']}-${AcceleratorStage.PIPELINE}-stack-${options.accountId}-${options.region}`, + ] + : [`${AcceleratorStackNames[options.stage]}-${options.accountId}-${options.region}`]; + } + + if (options.stage === AcceleratorStage.TESTER_PIPELINE) { + stackName = process.env['ACCELERATOR_QUALIFIER'] + ? [ + `${process.env['ACCELERATOR_QUALIFIER']}-${AcceleratorStage.TESTER_PIPELINE}-stack-${options.accountId}-${options.region}`, + ] + : [`${AcceleratorStackNames[options.stage]}-${options.accountId}-${options.region}`]; + } + + if (options.stage === AcceleratorStage.NETWORK_VPC) { + stackName = [ + `${AcceleratorStackNames[AcceleratorStage.NETWORK_VPC_DNS]}-${options.accountId}-${options.region}`, + ]; + } + + const selector: StackSelector = { + // patterns: [`${AcceleratorStackNames[options.stage]}-${options.accountId}-${options.region}`], + patterns: stackName, + }; + + await cli.deploy({ + selector, + toolkitStackName, + requireApproval: options.requireApproval, + }); + break; + case Command.SYNTHESIZE: + case Command.SYNTH: + await cli.synth([], false, true); + break; + + default: + throw new Error(`Unsupported command: ${options.command}`); + } + } +} diff --git a/source/packages/@aws-accelerator/accelerator/lib/validate-environment-config.ts b/source/packages/@aws-accelerator/accelerator/lib/validate-environment-config.ts new file mode 100644 index 000000000..5ce1e5faa --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/lib/validate-environment-config.ts @@ -0,0 +1,134 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { v4 as uuidv4 } from 'uuid'; +import { Construct } from 'constructs'; +import { Duration } from 'aws-cdk-lib'; +import path = require('path'); + +export interface ValidateEnvironmentConfigProps { + readonly acceleratorConfigTable: cdk.aws_dynamodb.ITable; + readonly newOrgAccountsTable: cdk.aws_dynamodb.ITable; + readonly newCTAccountsTable: cdk.aws_dynamodb.ITable; + readonly controlTowerEnabled: boolean; + readonly commitId: string; + readonly stackName: string; + readonly region: string; + readonly managementAccountId: string; + readonly partition: string; + /** + * Custom resource lambda log group encryption key + */ + readonly kmsKey: cdk.aws_kms.Key; + /** + * Custom resource lambda log retention in days + */ + readonly logRetentionInDays: number; +} + +/** + * Class Validate Environment Config + */ +export class ValidateEnvironmentConfig extends Construct { + public readonly id: string; + + constructor(scope: Construct, id: string, props: ValidateEnvironmentConfigProps) { + super(scope, id); + + const VALIDATE_ENVIRONMENT_RESOURCE_TYPE = 'Custom::ValidateEnvironmentConfig'; + + // + // Function definition for the custom resource + // + const provider = cdk.CustomResourceProvider.getOrCreateProvider(this, VALIDATE_ENVIRONMENT_RESOURCE_TYPE, { + codeDirectory: path.join(__dirname, 'lambdas/validate-environment/dist'), + runtime: cdk.CustomResourceProviderRuntime.NODEJS_14_X, + timeout: Duration.minutes(10), + policyStatements: [ + { + Sid: 'organizations', + Effect: 'Allow', + Action: [ + 'organizations:ListAccounts', + 'servicecatalog:SearchProvisionedProducts', + 'organizations:ListChildren', + ], + Resource: '*', + }, + { + Sid: 'dynamodb', + Effect: 'Allow', + Action: ['dynamodb:PutItem'], + Resource: [props.newOrgAccountsTable.tableArn, props.newCTAccountsTable?.tableArn], + }, + { + Sid: 'dynamodbConfigTable', + Effect: 'Allow', + Action: ['dynamodb:Query', 'dynamodb:UpdateItem'], + Resource: [props.acceleratorConfigTable.tableArn], + }, + { + Sid: 'kms', + Effect: 'Allow', + Action: ['kms:Encrypt', 'kms:Decrypt', 'kms:GenerateDataKey*', 'kms:DescribeKey'], + Resource: [props.newOrgAccountsTable.encryptionKey?.keyArn, props.newCTAccountsTable.encryptionKey?.keyArn], + }, + { + Sid: 'cloudformation', + Effect: 'Allow', + Action: ['cloudformation:DescribeStacks'], + Resource: [ + `arn:${props.partition}:cloudformation:${props.region}:${props.managementAccountId}:stack/${props.stackName}*`, + ], + }, + ], + }); + + // + // Custom Resource definition. We want this resource to be evaluated on + // every CloudFormation update, so we generate a new uuid to force + // re-evaluation. + // + const resource = new cdk.CustomResource(this, 'Resource', { + resourceType: VALIDATE_ENVIRONMENT_RESOURCE_TYPE, + serviceToken: provider.serviceToken, + properties: { + configTableName: props.acceleratorConfigTable.tableName, + newOrgAccountsTableName: props.newOrgAccountsTable.tableName, + newCTAccountsTableName: props.newCTAccountsTable?.tableName || '', + controlTowerEnabled: props.controlTowerEnabled, + commitId: props.commitId, + stackName: props.stackName, + uuid: uuidv4(), // Generates a new UUID to force the resource to update + }, + }); + + /** + * Singleton pattern to define the log group for the singleton function + * in the stack + */ + const stack = cdk.Stack.of(scope); + const logGroup = + (stack.node.tryFindChild(`${provider.node.id}LogGroup`) as cdk.aws_logs.LogGroup) ?? + new cdk.aws_logs.LogGroup(stack, `${provider.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${(provider.node.findChild('Handler') as cdk.aws_lambda.CfnFunction).ref}`, + retention: props.logRetentionInDays, + encryptionKey: props.kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + resource.node.addDependency(logGroup); + + this.id = resource.ref; + } +} diff --git a/source/packages/@aws-accelerator/accelerator/package.json b/source/packages/@aws-accelerator/accelerator/package.json new file mode 100644 index 000000000..78d2b9dc9 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/package.json @@ -0,0 +1,77 @@ +{ + "name": "@aws-accelerator/accelerator", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "private": true, + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "tsc", + "watch": "tsc -w", + "test": "jest --coverage --ci", + "lint": "eslint --fix --max-warnings 0 -c ../../../.eslintrc.json 'lib/**/*.{ts,tsx}'", + "precommit": "eslint --max-warnings 0 -c ../../../.eslintrc.json 'lib/**/*.{ts,tsx}'" + }, + "dependencies": { + "@aws-accelerator/config": "^0.0.0", + "@aws-accelerator/constructs": "^0.0.0", + "@aws-cdk-extensions/cdk-plugin-assume-role": "^0.0.0", + "aws-cdk-lib": "2.16.0", + "aws-cdk": "2.16.0", + "@types/aws-lambda": "8.10.86", + "@types/js-yaml": "4.0.5", + "@types/semver": "7.3.9", + "aws-lambda": "1.0.7", + "change-case": "4.1.2", + "fs-extra": "10.0.0", + "hash-sum": "2.0.0", + "io-ts": "2.2.16", + "io-ts-types": "0.5.16", + "js-yaml": "4.1.0", + "pascal-case": "3.1.2", + "semver": "7.3.5", + "tempy": "2.0.0", + "winston": "3.3.3", + "yargs": "17.3.0" + }, + "devDependencies": { + "aws-cdk-lib": "2.16.0", + "aws-cdk": "2.16.0", + "@aws-cdk/cx-api": "2.16.0", + "@aws-cdk/region-info": "2.16.0", + "cdk-assets": "2.16.0", + "chokidar": "3.5.3", + "constructs": "10.0.12", + "@types/fs-extra": "9.0.13", + "@types/jest": "27.0.3", + "@types/mri": "1.1.1", + "@types/node": "16.11.12", + "@types/promptly": "3.0.2", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "mri": "1.2.0", + "prettier": "2.5.1", + "promptly": "3.0.2", + "proxy-agent": "5.0.0", + "ts-jest": "27.1.1", + "ts-node": "10.4.0", + "typescript": "4.5.2" + }, + "peerDependencies": { + "eslint": "8.4.1" + } +} diff --git a/source/packages/@aws-accelerator/accelerator/test/__snapshots__/operations-stack.test.ts.snap b/source/packages/@aws-accelerator/accelerator/test/__snapshots__/operations-stack.test.ts.snap new file mode 100644 index 000000000..3f84c4aee --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/test/__snapshots__/operations-stack.test.ts.snap @@ -0,0 +1,273 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`OperationsStack Construct(OperationsStack): Snapshot Test 1`] = ` +Object { + "Resources": Object { + "AdministratorsA37EF73A": Object { + "Metadata": Object { + "cdk_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "AwsSolutions-IAM4", + "reason": "Groups created as per accelerator iam-config needs AWS managed policy", + }, + ], + }, + }, + "Properties": Object { + "GroupName": "Administrators", + "ManagedPolicyArns": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/AdministratorAccess", + ], + ], + }, + ], + }, + "Type": "AWS::IAM::Group", + }, + "BreakGlassUser01AA051328": Object { + "Properties": Object { + "Groups": Array [ + Object { + "Ref": "AdministratorsA37EF73A", + }, + ], + "LoginProfile": Object { + "Password": Object { + "Fn::Join": Array [ + "", + Array [ + "{{resolve:secretsmanager:", + Object { + "Ref": "BreakGlassUser01Secret8A54324D", + }, + ":SecretString:::}}", + ], + ], + }, + }, + "PermissionsBoundary": Object { + "Ref": "DefaultBoundaryPolicy489A8D26", + }, + "UserName": "breakGlassUser01", + }, + "Type": "AWS::IAM::User", + }, + "BreakGlassUser01Secret8A54324D": Object { + "DeletionPolicy": "Delete", + "Metadata": Object { + "cdk_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "AwsSolutions-SMG4", + "reason": "Accelerator users created as per iam-config file, upcoming change will take care of secret automatic rotation", + }, + ], + }, + }, + "Properties": Object { + "GenerateSecretString": Object { + "GenerateStringKey": "password", + "SecretStringTemplate": "{\\"username\\":\\"breakGlassUser01\\"}", + }, + "Name": "/accelerator/breakGlassUser01", + }, + "Type": "AWS::SecretsManager::Secret", + "UpdateReplacePolicy": "Delete", + }, + "BreakGlassUser02DFF444C8": Object { + "Properties": Object { + "Groups": Array [ + Object { + "Ref": "AdministratorsA37EF73A", + }, + ], + "LoginProfile": Object { + "Password": Object { + "Fn::Join": Array [ + "", + Array [ + "{{resolve:secretsmanager:", + Object { + "Ref": "BreakGlassUser02Secret4D200D8D", + }, + ":SecretString:::}}", + ], + ], + }, + }, + "PermissionsBoundary": Object { + "Ref": "DefaultBoundaryPolicy489A8D26", + }, + "UserName": "breakGlassUser02", + }, + "Type": "AWS::IAM::User", + }, + "BreakGlassUser02Secret4D200D8D": Object { + "DeletionPolicy": "Delete", + "Metadata": Object { + "cdk_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "AwsSolutions-SMG4", + "reason": "Accelerator users created as per iam-config file, upcoming change will take care of secret automatic rotation", + }, + ], + }, + }, + "Properties": Object { + "GenerateSecretString": Object { + "GenerateStringKey": "password", + "SecretStringTemplate": "{\\"username\\":\\"breakGlassUser02\\"}", + }, + "Name": "/accelerator/breakGlassUser02", + }, + "Type": "AWS::SecretsManager::Secret", + "UpdateReplacePolicy": "Delete", + }, + "DefaultBoundaryPolicy489A8D26": Object { + "Metadata": Object { + "cdk_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "AwsSolutions-IAM5", + "reason": "Policies definition are derived from accelerator iam-config boundary-policy file", + }, + ], + }, + }, + "Properties": Object { + "Description": "", + "ManagedPolicyName": "Default-Boundary-Policy", + "Path": "/", + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "*", + "Effect": "Allow", + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::IAM::ManagedPolicy", + }, + "Ec2DefaultSsmAdRoleADFFA4C6": Object { + "Metadata": Object { + "cdk_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "AwsSolutions-IAM4", + "reason": "IAM Role created as per accelerator iam-config needs AWS managed policy", + }, + ], + }, + }, + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "ec2.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/AmazonSSMManagedInstanceCore", + ], + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/AmazonSSMDirectoryServiceAccess", + ], + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/CloudWatchAgentServerPolicy", + ], + ], + }, + ], + "PermissionsBoundary": Object { + "Ref": "DefaultBoundaryPolicy489A8D26", + }, + "RoleName": "EC2-Default-SSM-AD-Role", + }, + "Type": "AWS::IAM::Role", + }, + "Ec2DefaultSsmAdRoleInstanceProfile": Object { + "Properties": Object { + "InstanceProfileName": Object { + "Ref": "Ec2DefaultSsmAdRoleADFFA4C6", + }, + "Roles": Array [ + Object { + "Ref": "Ec2DefaultSsmAdRoleADFFA4C6", + }, + ], + }, + "Type": "AWS::IAM::InstanceProfile", + }, + "ProviderSamlProviderDA84AD16": Object { + "Properties": Object { + "Name": "provider", + "SamlMetadataDocument": "", + }, + "Type": "AWS::IAM::SAMLProvider", + }, + "SsmParamAcceleratorVersionFF83282D": Object { + "Properties": Object { + "Name": "/accelerator/AWSAccelerator-OperationsStack-333333333333-us-east-1/version", + "Type": "String", + "Value": "1.0.0", + }, + "Type": "AWS::SSM::Parameter", + }, + "SsmParamStackId521A78D3": Object { + "Properties": Object { + "Name": "/accelerator/AWSAccelerator-OperationsStack-333333333333-us-east-1/stack-id", + "Type": "String", + "Value": Object { + "Ref": "AWS::StackId", + }, + }, + "Type": "AWS::SSM::Parameter", + }, + }, +} +`; diff --git a/source/packages/@aws-accelerator/accelerator/test/accounts-stack.test.ts b/source/packages/@aws-accelerator/accelerator/test/accounts-stack.test.ts new file mode 100644 index 000000000..89d358554 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/test/accounts-stack.test.ts @@ -0,0 +1,711 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { AccountsStack } from '../lib/stacks/accounts-stack'; +import { AcceleratorStackNames } from '../lib/accelerator'; +import { AcceleratorStage } from '../lib/accelerator-stage'; +import { + ACCOUNT_CONFIG, + GLOBAL_CONFIG, + IAM_CONFIG, + NETWORK_CONFIG, + ORGANIZATION_CONFIG, + SECURITY_CONFIG, +} from './configs/test-config'; +import * as path from 'path'; +import { AcceleratorStackProps } from '../lib/stacks/accelerator-stack'; + +//import { SynthUtils } from '@aws-cdk/assert'; + +const testNamePrefix = 'Construct(AccountsStack): '; + +/** + * AccountsStack + */ +const app = new cdk.App({ + context: { 'config-dir': path.join(__dirname, 'configs') }, +}); +const configDirPath = app.node.tryGetContext('config-dir'); + +const env = { + account: '333333333333', + region: 'us-east-1', +}; + +const props: AcceleratorStackProps = { + env, + configDirPath, + accountsConfig: ACCOUNT_CONFIG, + globalConfig: GLOBAL_CONFIG, + iamConfig: IAM_CONFIG, + networkConfig: NETWORK_CONFIG, + organizationConfig: ORGANIZATION_CONFIG, + securityConfig: SECURITY_CONFIG, + partition: 'aws', +}; + +const stack = new AccountsStack( + app, + `${AcceleratorStackNames[AcceleratorStage.ACCOUNTS]}-${env.account}-${env.region}`, + props, +); + +/** + * AccountsStack construct test + */ + +describe('AccountsStack', () => { + // test(`${testNamePrefix} Snapshot Test`, () => { + // expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); + // }); + + /** + * Number of AttachPolicy custom resource test + */ + test(`${testNamePrefix} AttachPolicy custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::AttachPolicy', 2); + }); + + /** + * Number of SSM parameters resource test + */ + test(`${testNamePrefix} SSM parameters resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::SSM::Parameter', 3); + }); + + /** + * Number of InviteAccountToOrganization custom resource test + */ + test(`${testNamePrefix} InviteAccountToOrganization custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::InviteAccountToOrganization', 3); + }); + + /** + * Number of Lambda Function resource test + */ + test(`${testNamePrefix} Lambda Function resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 5); + }); + + /** + * Number of IAM role resource test + */ + test(`${testNamePrefix} IAM role resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 5); + }); + + /** + * Number of CreatePolicy custom resource test + */ + test(`${testNamePrefix} CreatePolicy custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::CreatePolicy', 2); + }); + + /** + * Number of EnablePolicyType custom resource test + */ + test(`${testNamePrefix} EnablePolicyType custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::EnablePolicyType', 1); + }); + + /** + * AttachDenyDeleteVpcFlowLogsManagement custom resource configuration test + */ + test(`${testNamePrefix} AttachDenyDeleteVpcFlowLogsManagement resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + AttachDenyDeleteVpcFlowLogsManagementEF9C9CA8: { + Type: 'Custom::AttachPolicy', + UpdateReplacePolicy: 'Delete', + DeletionPolicy: 'Delete', + DependsOn: [ + 'CustomOrganizationsAttachPolicyCustomResourceProviderLogGroup03FEC039', + 'ManagementOrganizationAccount93F8866A', + ], + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomOrganizationsAttachPolicyCustomResourceProviderHandlerB3233202', 'Arn'], + }, + policyId: { + Ref: 'DenyDeleteVpcFlowLogsD2E9D6EC', + }, + targetId: '333333333333', + type: 'SERVICE_CONTROL_POLICY', + }, + }, + }, + }); + }); + + /** + * AttachDenyDeleteVpcFlowLogsSecurity custom resource configuration test + */ + test(`${testNamePrefix} AttachDenyDeleteVpcFlowLogsSecurity resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + AttachDenyDeleteVpcFlowLogsSecurity37915A30: { + Type: 'Custom::AttachPolicy', + UpdateReplacePolicy: 'Delete', + DeletionPolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomOrganizationsAttachPolicyCustomResourceProviderHandlerB3233202', 'Arn'], + }, + partition: { + Ref: 'AWS::Partition', + }, + policyId: { + Ref: 'DenyDeleteVpcFlowLogsD2E9D6EC', + }, + targetId: 'Security-id', + type: 'SERVICE_CONTROL_POLICY', + }, + }, + }, + }); + }); + + /** + * AuditOrganizationAccount custom resource configuration test + */ + test(`${testNamePrefix} AuditOrganizationAccount custom resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + AuditOrganizationAccount11304D9B: { + Type: 'Custom::InviteAccountToOrganization', + UpdateReplacePolicy: 'Delete', + DeletionPolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomInviteAccountToOrganizationCustomResourceProviderHandlerAEB26818', 'Arn'], + }, + accountId: '222222222222', + roleArn: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::222222222222:role/AWSControlTowerExecution', + ], + ], + }, + }, + }, + }, + }); + }); + + /** + * CustomEnablePolicyTypeCustomResourceProviderHandler lambda function resource configuration test + */ + test(`${testNamePrefix} CustomEnablePolicyTypeCustomResourceProviderHandler lambda function resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomEnablePolicyTypeCustomResourceProviderHandlerC244F9E1: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomEnablePolicyTypeCustomResourceProviderRoleAE71B2CA'], + Properties: { + Code: { + S3Bucket: 'cdk-hnb659fds-assets-333333333333-us-east-1', + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomEnablePolicyTypeCustomResourceProviderRoleAE71B2CA', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * CustomEnablePolicyTypeCustomResourceProviderRole resource configuration test + */ + test(`${testNamePrefix} CustomEnablePolicyTypeCustomResourceProviderRole resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomEnablePolicyTypeCustomResourceProviderRoleAE71B2CA: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: [ + 'organizations:DescribeOrganization', + 'organizations:DisablePolicyType', + 'organizations:EnablePolicyType', + 'organizations:ListRoots', + 'organizations:ListPoliciesForTarget', + 'organizations:ListTargetsForPolicy', + 'organizations:DescribeEffectivePolicy', + 'organizations:DescribePolicy', + 'organizations:DisableAWSServiceAccess', + 'organizations:DetachPolicy', + 'organizations:DeletePolicy', + 'organizations:DescribeAccount', + 'organizations:ListAWSServiceAccessForOrganization', + 'organizations:ListPolicies', + 'organizations:ListAccountsForParent', + 'organizations:ListAccounts', + 'organizations:EnableAWSServiceAccess', + 'organizations:ListCreateAccountStatus', + 'organizations:UpdatePolicy', + 'organizations:DescribeOrganizationalUnit', + 'organizations:AttachPolicy', + 'organizations:ListParents', + 'organizations:ListOrganizationalUnitsForParent', + 'organizations:CreatePolicy', + 'organizations:DescribeCreateAccountStatus', + ], + Effect: 'Allow', + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * CustomInviteAccountToOrganizationCustomResourceProviderHandler lambda function resource configuration test + */ + test(`${testNamePrefix} CustomInviteAccountToOrganizationCustomResourceProviderHandler lambda function resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomInviteAccountToOrganizationCustomResourceProviderHandlerAEB26818: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomInviteAccountToOrganizationCustomResourceProviderRole0F64F419'], + Properties: { + Code: { + S3Bucket: 'cdk-hnb659fds-assets-333333333333-us-east-1', + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomInviteAccountToOrganizationCustomResourceProviderRole0F64F419', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * CustomInviteAccountToOrganizationCustomResourceProviderRole resource configuration test + */ + test(`${testNamePrefix} CustomInviteAccountToOrganizationCustomResourceProviderRole resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomInviteAccountToOrganizationCustomResourceProviderRole0F64F419: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: [ + 'organizations:AcceptHandshake', + 'organizations:ListAccounts', + 'organizations:InviteAccountToOrganization', + 'organizations:MoveAccount', + 'sts:AssumeRole', + ], + Effect: 'Allow', + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * CustomOrganizationsAttachPolicyCustomResourceProviderHandler lambda function resource configuration test + */ + test(`${testNamePrefix} CustomOrganizationsAttachPolicyCustomResourceProviderHandler lambda function resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomOrganizationsAttachPolicyCustomResourceProviderHandlerB3233202: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomOrganizationsAttachPolicyCustomResourceProviderRole051E00A6'], + Properties: { + Code: { + S3Bucket: 'cdk-hnb659fds-assets-333333333333-us-east-1', + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomOrganizationsAttachPolicyCustomResourceProviderRole051E00A6', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * CustomOrganizationsAttachPolicyCustomResourceProviderRole resource configuration test + */ + test(`${testNamePrefix} CustomOrganizationsAttachPolicyCustomResourceProviderRole resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomOrganizationsAttachPolicyCustomResourceProviderRole051E00A6: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: [ + 'organizations:AttachPolicy', + 'organizations:DetachPolicy', + 'organizations:ListPoliciesForTarget', + ], + Effect: 'Allow', + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * CustomOrganizationsCreatePolicyCustomResourceProviderHandler lambda function resource configuration test + */ + test(`${testNamePrefix} CustomOrganizationsCreatePolicyCustomResourceProviderHandler lambda function resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomOrganizationsCreatePolicyCustomResourceProviderHandler7A188619: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomOrganizationsCreatePolicyCustomResourceProviderRoleBA0ADB43'], + Properties: { + Code: { + S3Bucket: 'cdk-hnb659fds-assets-333333333333-us-east-1', + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomOrganizationsCreatePolicyCustomResourceProviderRoleBA0ADB43', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * CustomOrganizationsCreatePolicyCustomResourceProviderRole resource configuration test + */ + test(`${testNamePrefix} CustomOrganizationsCreatePolicyCustomResourceProviderRole resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomOrganizationsCreatePolicyCustomResourceProviderRoleBA0ADB43: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: [ + 'organizations:CreatePolicy', + 'organizations:ListPolicies', + 'organizations:UpdatePolicy', + ], + Effect: 'Allow', + Resource: '*', + }, + { + Action: ['s3:GetObject'], + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':s3:::cdk-hnb659fds-assets-333333333333-us-east-1/*', + ], + ], + }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * DenyDeleteVpcFlowLogs custom resource configuration test + */ + test(`${testNamePrefix} DenyDeleteVpcFlowLogs custom resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + DenyDeleteVpcFlowLogsD2E9D6EC: { + Type: 'Custom::CreatePolicy', + UpdateReplacePolicy: 'Delete', + DeletionPolicy: 'Delete', + DependsOn: [ + 'CustomOrganizationsCreatePolicyCustomResourceProviderLogGroup019B74A9', + 'enablePolicyTypeScpB4BC96BE', + ], + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomOrganizationsCreatePolicyCustomResourceProviderHandler7A188619', 'Arn'], + }, + bucket: 'cdk-hnb659fds-assets-333333333333-us-east-1', + name: 'DenyDeleteVpcFlowLogs', + type: 'SERVICE_CONTROL_POLICY', + }, + }, + }, + }); + }); + + /** + * LogArchiveOrganizationAccount custom resource configuration test + */ + test(`${testNamePrefix} LogArchiveOrganizationAccount custom resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + LogArchiveOrganizationAccount09183FEA: { + Type: 'Custom::InviteAccountToOrganization', + UpdateReplacePolicy: 'Delete', + DeletionPolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomInviteAccountToOrganizationCustomResourceProviderHandlerAEB26818', 'Arn'], + }, + accountId: '111111111111', + roleArn: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::111111111111:role/AWSControlTowerExecution', + ], + ], + }, + }, + }, + }, + }); + }); + + /** + * ManagementOrganizationAccount custom resource configuration test + */ + test(`${testNamePrefix} ManagementOrganizationAccount custom resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + ManagementOrganizationAccount93F8866A: { + Type: 'Custom::InviteAccountToOrganization', + UpdateReplacePolicy: 'Delete', + DeletionPolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomInviteAccountToOrganizationCustomResourceProviderHandlerAEB26818', 'Arn'], + }, + accountId: '333333333333', + roleArn: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::333333333333:role/AWSControlTowerExecution', + ], + ], + }, + }, + }, + }, + }); + }); + + /** + * SSM parameter SsmParamAcceleratorVersion resource configuration test + */ + test(`${testNamePrefix} SSM parameter SsmParamAcceleratorVersion resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + SsmParamAcceleratorVersionFF83282D: { + Type: 'AWS::SSM::Parameter', + Properties: { + Name: '/accelerator/AWSAccelerator-AccountsStack-333333333333-us-east-1/version', + Type: 'String', + Value: '1.0.0', + }, + }, + }, + }); + }); + + /** + * SSM parameter SsmParamStackId resource configuration test + */ + test(`${testNamePrefix} SSM parameter SsmParamStackId resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + SsmParamStackId521A78D3: { + Type: 'AWS::SSM::Parameter', + Properties: { + Name: '/accelerator/AWSAccelerator-AccountsStack-333333333333-us-east-1/stack-id', + Type: 'String', + Value: { + Ref: 'AWS::StackId', + }, + }, + }, + }, + }); + }); + + /** + * EnablePolicyType custom resource configuration test + */ + test(`${testNamePrefix} EnablePolicyType custom resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + enablePolicyTypeScpB4BC96BE: { + Type: 'Custom::EnablePolicyType', + UpdateReplacePolicy: 'Delete', + DeletionPolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomEnablePolicyTypeCustomResourceProviderHandlerC244F9E1', 'Arn'], + }, + policyType: 'SERVICE_CONTROL_POLICY', + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/accelerator/test/configs/backup-policies/org-backup-policies.json b/source/packages/@aws-accelerator/accelerator/test/configs/backup-policies/org-backup-policies.json new file mode 100644 index 000000000..e43292c6b --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/test/configs/backup-policies/org-backup-policies.json @@ -0,0 +1,50 @@ +{ + "plans": { + "PII_Backup_Plan": { + "rules": { + "My_Hourly_Rule": { + "schedule_expression": {"@@assign": "cron(0 5 ? * * *)"}, + "start_backup_window_minutes": {"@@assign": "60"}, + "complete_backup_window_minutes": {"@@assign": "604800"}, + "enable_continuous_backup": {"@@assign": false}, + "target_backup_vault_name": {"@@assign": "AcceleratorBackupVault"}, + "recovery_point_tags": { + "Owner": { + "tag_key": {"@@assign": "Owner"}, + "tag_value": {"@@assign": "Backup"} + } + }, + "lifecycle": { + "move_to_cold_storage_after_days": {"@@assign": "180"}, + "delete_after_days": {"@@assign": "270"} + } + } + }, + "regions": { + "@@append": [ + "us-east-1" + ] + }, + "selections": { + "tags": { + "My_Backup_Assignment": { + "iam_role_arn": {"@@assign": "arn:aws:iam::$account:role/Backup-Role"}, + "tag_key": {"@@assign": "dataType"}, + "tag_value": { + "@@assign": [ + "PII", + "RED" + ] + } + } + } + }, + "backup_plan_tags": { + "stage": { + "tag_key": {"@@assign": "Stage"}, + "tag_value": {"@@assign": "Beta"} + } + } + } + } +} \ No newline at end of file diff --git a/source/packages/@aws-accelerator/accelerator/test/configs/iam-policies/boundary-policy.json b/source/packages/@aws-accelerator/accelerator/test/configs/iam-policies/boundary-policy.json new file mode 100644 index 000000000..12bf2188f --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/test/configs/iam-policies/boundary-policy.json @@ -0,0 +1,10 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "*", + "Resource": "*" + } + ] +} diff --git a/source/packages/@aws-accelerator/accelerator/test/configs/metadataDocument b/source/packages/@aws-accelerator/accelerator/test/configs/metadataDocument new file mode 100644 index 000000000..e69de29bb diff --git a/source/packages/@aws-accelerator/accelerator/test/configs/service-control-policies/deny-delete-vpc-flow-logs.json b/source/packages/@aws-accelerator/accelerator/test/configs/service-control-policies/deny-delete-vpc-flow-logs.json new file mode 100644 index 000000000..df4c5400d --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/test/configs/service-control-policies/deny-delete-vpc-flow-logs.json @@ -0,0 +1,15 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Deny", + "Action": ["ec2:DeleteFlowLogs", "logs:DeleteLogGroup", "logs:DeleteLogStream"], + "Resource": "*", + "Condition": { + "ArnNotLike": { + "aws:PrincipalArn": ["arn:aws:iam::*:role/cdk-accel-*"] + } + } + } + ] +} diff --git a/source/packages/@aws-accelerator/accelerator/test/configs/service-control-policies/quarantine.json b/source/packages/@aws-accelerator/accelerator/test/configs/service-control-policies/quarantine.json new file mode 100644 index 000000000..6c82bf078 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/test/configs/service-control-policies/quarantine.json @@ -0,0 +1,20 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "DenyAllAWSServicesExceptBreakglassRoles", + "Effect": "Deny", + "Action": "*", + "Resource": "*", + "Condition": { + "ArnNotLike": { + "aws:PrincipalARN": [ + "arn:aws:iam::*:role/${MANAGEMENT_ACCOUNT_ACCESS_ROLE}", + "arn:aws:iam::*:role/aws*", + "arn:aws:iam::*:role/${ACCELERATOR_PREFIX}*" + ] + } + } + } + ] +} diff --git a/source/packages/@aws-accelerator/accelerator/test/configs/tagging-policies/org-tag-policy.json b/source/packages/@aws-accelerator/accelerator/test/configs/tagging-policies/org-tag-policy.json new file mode 100644 index 000000000..e49fbb0f4 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/test/configs/tagging-policies/org-tag-policy.json @@ -0,0 +1,15 @@ +{ + "tags": { + "costcenter": { + "tag_key": { + "@@assign": "CostCenter" + }, + "tag_value": { + "@@assign": [ + "100", + "200" + ] + } + } + } +} diff --git a/source/packages/@aws-accelerator/accelerator/test/configs/test-config.ts b/source/packages/@aws-accelerator/accelerator/test/configs/test-config.ts new file mode 100644 index 000000000..bfb8e3292 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/test/configs/test-config.ts @@ -0,0 +1,973 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { GlobalConfig, IamConfig, NetworkConfig, SecurityConfig } from '@aws-accelerator/config'; + +const globalConfigJson = { + homeRegion: 'us-east-1', + enabledRegions: ['us-east-1'], + managementAccountAccessRole: 'AWSControlTowerExecution', + cloudwatchLogRetentionInDays: 3653, + controlTower: { + enable: true, + }, + logging: { + account: 'LogArchive', + cloudtrail: { + enable: false, + organizationTrail: false, + }, + sessionManager: { + sendToCloudWatchLogs: true, + sendToS3: true, + }, + }, + dataProtection: { + enable: true, + identityPerimeter: { enable: true }, + resourcePerimeter: { enable: true }, + networkPerimeter: { enable: true }, + }, + reports: { + costAndUsageReport: { + compression: 'Parquet', + format: 'Parquet', + reportName: 'TestReport', + s3Prefix: 'cur', + timeUnit: 'DAILY', + refreshClosedReports: true, + reportVersioning: 'OVERWRITE_REPORT', + }, + }, +}; + +export const GLOBAL_CONFIG = GlobalConfig.loadFromString(JSON.stringify(globalConfigJson))!; + +export const iamConfigJson = { + providers: [ + { + name: 'provider', + metadataDocument: 'metadataDocument', + }, + ], + policySets: [ + { + deploymentTargets: { + organizationalUnits: ['Root'], + accounts: ['Management'], + excludedRegions: [], + excludedAccounts: [], + }, + policies: [ + { + name: 'Default-Boundary-Policy', + policy: 'iam-policies/boundary-policy.json', + }, + ], + }, + ], + roleSets: [ + { + deploymentTargets: { + organizationalUnits: ['Root'], + accounts: ['Management'], + excludedRegions: [], + excludedAccounts: [], + }, + roles: [ + { + instanceProfile: true, + name: 'EC2-Default-SSM-AD-Role', + assumedBy: [ + { + type: 'service', + principal: 'ec2.amazonaws.com', + }, + ], + policies: { + awsManaged: [ + 'AmazonSSMManagedInstanceCore', + 'AmazonSSMDirectoryServiceAccess', + 'CloudWatchAgentServerPolicy', + ], + customerManaged: [], + }, + boundaryPolicy: 'Default-Boundary-Policy', + }, + ], + }, + ], + groupSets: [ + { + deploymentTargets: { + organizationalUnits: ['Root'], + accounts: ['Management'], + excludedRegions: [], + excludedAccounts: [], + }, + groups: [ + { + name: 'Administrators', + policies: { + awsManaged: ['AdministratorAccess'], + customerManaged: [], + }, + }, + ], + }, + ], + userSets: [ + { + deploymentTargets: { + organizationalUnits: ['Root'], + accounts: ['Management'], + excludedRegions: [], + excludedAccounts: [], + }, + users: [ + { + username: 'breakGlassUser01', + group: 'Administrators', + boundaryPolicy: 'Default-Boundary-Policy', + }, + { + username: 'breakGlassUser02', + group: 'Administrators', + boundaryPolicy: 'Default-Boundary-Policy', + }, + ], + }, + ], + ec2InstanceDefaultProfile: { + name: 'Accelerator-EC2-Instance-Default-Profile', + deploymentTargets: { + organizationalUnits: ['Root'], + }, + }, +}; + +export const IAM_CONFIG = IamConfig.loadFromString(JSON.stringify(iamConfigJson))!; + +const networkConfigJson = { + homeRegion: 'us-east-1', + defaultVpc: { + delete: true, + }, + transitGateways: [ + { + name: 'Main', + account: 'Management', + region: 'us-east-1', + shareTargets: { + organizationalUnits: ['Sandbox'], + accounts: ['Audit'], + }, + asn: 65521, + dnsSupport: 'enable', + vpnEcmpSupport: 'enable', + defaultRouteTableAssociation: 'disable', + defaultRouteTablePropagation: 'disable', + autoAcceptSharingAttachments: 'enable', + routeTables: [ + { + name: 'core', + routes: [ + { + destinationCidrBlock: '10.40.0.0/24', + attachment: { + vpcName: 'Test', + account: 'Audit', + }, + blackhole: false, + }, + ], + }, + { + name: 'segregated', + routes: [ + { + destinationCidrBlock: '10.40.0.0/24', + attachment: { + vpcName: 'Test', + account: 'Audit', + }, + blackhole: false, + }, + ], + }, + { + name: 'shared', + routes: [ + { + destinationCidrBlock: '10.40.0.0/24', + attachment: { + vpcName: 'Test', + account: 'Audit', + }, + blackhole: false, + }, + ], + }, + { + name: 'standalone', + routes: [ + { + destinationCidrBlock: '10.40.0.0/24', + attachment: { + vpcName: 'Test', + account: 'Audit', + }, + blackhole: false, + }, + ], + }, + ], + }, + ], + centralNetworkServices: { + delegatedAdminAccount: 'Audit', + networkFirewall: { + firewalls: [ + { + name: 'Accelerator-Firewall', + region: 'us-east-1', + firewallPolicy: 'Accelerator-Policy', + subnets: ['public-a', 'public-b'], + vpc: 'Test', + }, + ], + policies: [ + { + name: 'Accelerator-Policy', + regions: ['us-east-1'], + firewallPolicy: { + statelessDefaultActions: ['aws:forward_to_sfe'], + statelessFragmentDefaultActions: ['aws:forward_to_sfe'], + }, + }, + ], + rules: [ + { + name: 'Accelerator-Rule', + regions: ['us-east-1'], + capacity: 100, + type: 'STATEFUL', + ruleGroup: { + rulesSource: { + rulesSourceList: { + generatedRulesType: 'DENYLIST', + targets: ['.example.com'], + targetTypes: ['TLS_SNI', 'HTTP_HOST'], + }, + ruleVariables: { + ipSets: { + name: 'HOME_NET', + definition: ['10.0.0.0/16', '10.1.0.0/16'], + }, + portSets: { + name: 'HOME_NET', + definition: ['80', '443'], + }, + }, + }, + }, + }, + ], + }, + route53Resolver: { + endpoints: [ + { + name: 'Accelerator-Inbound', + type: 'INBOUND', + vpc: 'Test', + subnets: ['public-a', 'public-b'], + }, + { + name: 'Accelerator-Outbound', + type: 'OUTBOUND', + vpc: 'Test', + subnets: ['public-a', 'public-b'], + rules: [ + { + name: 'example-rule', + domainName: 'example.com', + targetIps: [ + { + ip: '1.1.1.1', + }, + { + ip: '2.2.2.2', + }, + ], + shareTargets: { + organizationalUnits: ['Sandbox'], + }, + }, + ], + }, + ], + queryLogs: { + name: 'Accelerator-Query-Logs', + destinations: ['s3', 'cloud-watch-logs'], + shareTargets: { + organizationalUnits: ['Sandbox'], + }, + }, + firewallRuleGroups: [ + { + name: 'Accelerator-Block-Group', + regions: ['us-east-1'], + rules: [ + { + name: 'Custom-Rule', + action: 'BLOCK', + customDomainList: 'dns-firewall-rule-groups/domain-list.txt', + priority: 100, + blockResponse: 'NXDOMAIN', + }, + { + name: 'Managed-Rule', + action: 'BLOCK', + managedDomainList: 'AWSManagedDomainsBotnetCommandandControl', + priority: 200, + blockResponse: 'NODATA', + }, + ], + shareTargets: { + organizationalUnits: ['Sandbox'], + }, + }, + ], + }, + }, + prefixLists: [ + { + name: 'Test', + accounts: ['Management'], + regions: ['us-east-1'], + addressFamily: 'IPv4', + maxEntries: 10, + entries: ['10.0.0.0/8', '100.96.252.0/23', '100.96.250.0/23'], + }, + ], + endpointPolicies: [ + { + name: 'Default', + document: 'vpc-endpoint-policies/default.json', + }, + ], + vpcs: [ + { + name: 'Test', + account: 'Audit', + region: 'us-east-1', + cidrs: ['10.0.0.0/16'], + internetGateway: true, + enableDnsHostnames: false, + enableDnsSupport: true, + dnsFirewallRuleGroups: [ + { + name: 'Accelerator-Block-Group', + priority: 101, + }, + ], + queryLogs: ['Accelerator-Query-Logs'], + resolverRules: ['example-rule'], + instanceTenancy: 'default', + routeTables: [ + { + name: 'CentralVpc_Common', + routes: [ + { + name: 'TgwRoute', + destination: '10.0.0.0/8', + type: 'transitGateway', + target: 'Main', + }, + { + name: 'NatRoute', + destination: '192.168.0.0/16', + type: 'natGateway', + target: 'Web-A', + }, + { + name: 'IgwRoute', + destination: '0.0.0.0/0', + type: 'internetGateway', + target: 'IGW', + }, + { + name: 's3', + target: 's3', + }, + { + name: 'dynamodb', + target: 'dynamodb', + }, + ], + }, + ], + subnets: [ + { + name: 'public-a', + availabilityZone: 'a', + mapPublicIpOnLaunch: true, + routeTable: 'CentralVpc_Common', + ipv4CidrBlock: '10.0.0.0/24', + }, + { + name: 'public-b', + availabilityZone: 'b', + mapPublicIpOnLaunch: true, + routeTable: 'CentralVpc_Common', + ipv4CidrBlock: '10.0.1.0/24', + }, + { + name: 'tgw-attach-a', + availabilityZone: 'a', + routeTable: 'CentralVpc_Common', + ipv4CidrBlock: '10.0.2.0/24', + }, + { + name: 'tgw-attach-b', + availabilityZone: 'b', + routeTable: 'CentralVpc_Common', + ipv4CidrBlock: '10.0.3.0/24', + }, + ], + natGateways: [ + { + name: 'Web-A', + subnet: 'public-a', + }, + { + name: 'Web-B', + subnet: 'public-b', + }, + ], + transitGatewayAttachments: [ + { + name: 'Test', + transitGateway: { + name: 'Main', + account: 'Management', + }, + routeTableAssociations: ['shared'], + routeTablePropagations: ['core', 'shared', 'segregated'], + subnets: ['tgw-attach-a', 'tgw-attach-b'], + }, + ], + dnsResolverLogging: true, + useCentralEndpoints: false, + gatewayEndpoints: { + defaultPolicy: 'Default', + endpoints: [ + { + service: 's3', + }, + { + service: 'dynamodb', + }, + ], + }, + securityGroups: [ + { + name: 'Management', + description: 'Management Security Group', + inboundRules: [ + { + description: 'Management RDP Traffic Inbound', + types: ['RDP'], + sources: [ + '10.0.0.0/8', + '100.96.252.0/23', + '100.96.250.0/23', + { + account: 'Audit', + vpc: 'Test', + subnets: ['tgw-attach-a', 'tgw-attach-b'], + }, + { + securityGroups: ['Management'], + }, + ], + }, + { + description: 'Management SSH Traffic Inbound', + types: ['SSH'], + sources: [ + { + prefixLists: ['Test'], + }, + ], + }, + ], + outboundRules: [ + { + description: 'All Outbound', + types: ['ALL'], + sources: ['0.0.0.0/0'], + }, + ], + }, + ], + }, + { + name: 'Test-Peer', + account: 'Audit', + region: 'us-east-1', + cidrs: ['10.1.0.0/16'], + internetGateway: true, + enableDnsHostnames: false, + enableDnsSupport: true, + instanceTenancy: 'default', + routeTables: [], + subnets: [], + natGateways: [], + transitGatewayAttachments: [], + useCentralEndpoints: false, + securityGroups: [], + }, + ], + vpcFlowLogs: { + trafficType: 'ALL', + maxAggregationInterval: 60, + destinations: ['s3', 'cloud-watch-logs'], + defaultFormat: false, + customFields: [ + 'version', + 'account-id', + 'interface-id', + 'srcaddr', + 'dstaddr', + 'srcport', + 'dstport', + 'protocol', + 'packets', + 'bytes', + 'start', + 'end', + 'action', + 'log-status', + 'vpc-id', + 'subnet-id', + 'instance-id', + 'tcp-flags', + 'type', + 'pkt-srcaddr', + 'pkt-dstaddr', + 'region', + 'az-id', + 'pkt-src-aws-service', + 'pkt-dst-aws-service', + 'flow-direction', + 'traffic-path', + ], + }, + vpcPeering: [ + { + name: 'Test', + vpcs: ['Test', 'Test-Peer'], + }, + ], +}; + +export const NETWORK_CONFIG = NetworkConfig.loadFromString(JSON.stringify(networkConfigJson))!; + +const securityConfigJson = { + homeRegion: 'us-east-1', + centralSecurityServices: { + delegatedAdminAccount: 'Audit', + ebsDefaultVolumeEncryption: { + enable: true, + excludeRegions: [], + }, + s3PublicAccessBlock: { + enable: true, + excludeAccounts: [], + }, + snsSubscriptions: [ + { + level: 'High', + email: 'highalert@amazon.com', + }, + { + level: 'Medium', + email: 'midalert@amazon.com', + }, + { + level: 'Low', + email: 'lowalert@amazon.com', + }, + ], + macie: { + enable: true, + excludeRegions: [], + policyFindingsPublishingFrequency: 'FIFTEEN_MINUTES', + publishSensitiveDataFindings: true, + }, + guardduty: { + enable: true, + excludeRegions: [], + s3Protection: { + enable: true, + excludeRegions: [], + }, + exportConfiguration: { + enable: true, + destinationType: 'S3', + exportFrequency: 'FIFTEEN_MINUTES', + }, + }, + securityHub: { + enable: true, + excludeRegions: [], + standards: [ + { + name: 'AWS Foundational Security Best Practices v1.0.0', + enable: true, + controlsToDisable: ['IAM.1', 'EC2.10', 'Lambda.4'], + }, + { + name: 'PCI DSS v3.2.1', + enable: true, + controlsToDisable: ['PCI.IAM.3', 'PCI.S3.3', 'PCI.EC2.3', 'PCI.Lambda.2'], + }, + { + name: 'CIS AWS Foundations Benchmark v1.2.0', + enable: true, + controlsToDisable: ['CIS.1.20', 'CIS.1.22', 'CIS.2.6'], + }, + ], + }, + ssmAutomation: { + excludeRegions: [], + documentSets: [], + }, + }, + accessAnalyzer: { + enable: true, + }, + iamPasswordPolicy: { + allowUsersToChangePassword: true, + hardExpiry: false, + requireUppercaseCharacters: true, + requireLowercaseCharacters: true, + requireSymbols: true, + requireNumbers: true, + minimumPasswordLength: 14, + passwordReusePrevention: 24, + maxPasswordAge: 90, + }, + awsConfig: { + enableConfigurationRecorder: true, + enableDeliveryChannel: true, + ruleSets: [ + { + deploymentTargets: { + organizationalUnits: ['Root'], + }, + rules: [ + { + name: 'accelerator-iam-user-group-membership-check', + complianceResourceTypes: ['AWS::IAM::User'], + identifier: 'IAM_USER_GROUP_MEMBERSHIP_CHECK', + }, + { + name: 'accelerator-securityhub-enabled', + identifier: 'SECURITYHUB_ENABLED', + }, + { + name: 'accelerator-cloudtrail-enabled', + identifier: 'CLOUD_TRAIL_ENABLED', + }, + ], + }, + ], + }, + cloudWatch: { + metricSets: [ + { + regions: ['us-east-1'], + deploymentTargets: { + organizationalUnits: ['Root'], + }, + metrics: [ + { + filterName: 'RootAccountMetricFilter', + logGroupName: 'aws-controltower/CloudTrailLogs', + filterPattern: + '{$.userIdentity.type="Root" && $.userIdentity.invokedBy NOT EXISTS && $.eventType !="AwsServiceEvent"}', + metricNamespace: 'LogMetrics', + metricName: 'RootAccount', + metricValue: '1', + }, + { + filterName: 'UnauthorizedAPICallsMetricFilter', + logGroupName: 'aws-controltower/CloudTrailLogs', + filterPattern: '{($.errorCode="*UnauthorizedOperation") || ($.errorCode="AccessDenied*")}', + metricNamespace: 'LogMetrics', + metricName: 'UnauthorizedAPICalls', + metricValue: '1', + }, + { + filterName: 'ConsoleSigninWithoutMFAMetricFilter', + logGroupName: 'aws-controltower/CloudTrailLogs', + filterPattern: '{($.eventName="ConsoleLogin") && ($.additionalEventData.MFAUsed !="Yes")}', + metricNamespace: 'LogMetrics', + metricName: 'ConsoleSigninWithoutMFA', + metricValue: '1', + }, + ], + }, + ], + alarmSets: [ + { + regions: ['us-east-1'], + deploymentTargets: { + organizationalUnits: ['Root'], + }, + alarms: [ + { + alarmName: 'CIS-1.1-RootAccountUsage', + alarmDescription: 'Alarm for usage of "root" account', + snsAlertLevel: 'Low', + metricName: 'RootAccountUsage', + namespace: 'LogMetrics', + comparisonOperator: 'GreaterThanOrEqualToThreshold', + evaluationPeriods: 1, + period: 300, + statistic: 'Sum', + threshold: 1, + treatMissingData: 'notBreaching', + }, + { + alarmName: 'CIS-3.1-UnauthorizedAPICalls', + alarmDescription: 'Alarm for unauthorized API calls', + snsAlertLevel: 'Low', + metricName: 'UnauthorizedAPICalls', + namespace: 'LogMetrics', + comparisonOperator: 'GreaterThanOrEqualToThreshold', + evaluationPeriods: 1, + period: 300, + statistic: 'Sum', + threshold: 1, + treatMissingData: 'notBreaching', + }, + { + alarmName: 'CIS-3.2-ConsoleSigninWithoutMFA', + alarmDescription: 'Alarm for AWS Management Console sign-in without MFA', + snsAlertLevel: 'Low', + metricName: 'ConsoleSigninWithoutMFA', + namespace: 'LogMetrics', + comparisonOperator: 'GreaterThanOrEqualToThreshold', + evaluationPeriods: 1, + period: 300, + statistic: 'Sum', + threshold: 1, + treatMissingData: 'notBreaching', + }, + ], + }, + ], + }, +}; + +export const SECURITY_CONFIG = SecurityConfig.loadFromString(JSON.stringify(securityConfigJson))!; + +export const ORGANIZATION_CONFIG = { + enable: true as const, + organizationalUnits: [ + { + name: 'Security', + }, + { + name: 'Sandbox', + }, + ], + organizationalUnitIds: [{ name: 'Security', id: 'securityOrgId', arn: 'securityOrgArn' }], + quarantineNewAccounts: { + enable: true, + scpPolicyName: 'QuarantineAccounts', + }, + serviceControlPolicies: [ + { + name: 'DenyDeleteVpcFlowLogs', + description: + 'This SCP prevents users or roles in any affected account from deleting Amazon Elastic Compute Cloud (Amazon EC2) flow logs or CloudWatch log groups or log streams.\n', + policy: 'service-control-policies/deny-delete-vpc-flow-logs.json', + type: 'customerManaged', + deploymentTargets: { + organizationalUnits: ['Security'], + accounts: ['Management'], + excludedRegions: [], + excludedAccounts: [], + }, + }, + { + name: 'QuarantineAccounts', + description: + 'This SCP is used to prevent changes to new accounts until the accelerator\nhas been executed successfully.\nThis policy will be applied upon account create if enabled.\n', + policy: 'service-control-policies/quarantine.json', + type: 'customerManaged', + deploymentTargets: { + organizationalUnits: [], + accounts: [], + excludedRegions: [], + excludedAccounts: [], + }, + }, + ], + taggingPolicies: [ + { + name: 'OrgTagPolicy', + description: 'Organization Tagging Policy', + policy: 'tagging-policies/org-tag-policy.json', + deploymentTargets: { + organizationalUnits: ['Root'], + accounts: ['Management'], + excludedRegions: [], + excludedAccounts: [], + }, + }, + ], + backupPolicies: [ + { + name: 'OrgBackupPolicy', + description: 'Organization Backup Policy', + policy: 'backup-policies/org-backup-policies.json', + deploymentTargets: { + organizationalUnits: ['Root'], + accounts: ['Management'], + excludedRegions: [], + excludedAccounts: [], + }, + }, + ], + // eslint-disable-next-line @typescript-eslint/no-empty-function + loadOrganizationalUnitIds: async function (): Promise {}, + getOrganizationalUnitId: function (name: string) { + return name + '-id'; + }, + getOrganizationalUnitArn: function (name: string) { + return name + '-arn'; + }, + getPath: function (name: string) { + return name + '-path'; + }, + getOuName: function (name: string) { + return name; + }, + getParentOuName: function (name: string) { + return name + '-parent'; + }, +}; + +export const ACCOUNT_CONFIG = { + mandatoryAccounts: [ + { + name: 'Management', + description: 'The management (primary) account', + email: 'management-account@amazon.com', + organizationalUnit: 'Root', + }, + { + name: 'LogArchive', + description: 'The log archive account', + email: 'logarchive-account@amazon.com', + organizationalUnit: 'Security', + }, + { + name: 'Audit', + description: 'The security audit account (also referred to as the audit account)', + email: 'audit-account@amazon.com', + organizationalUnit: 'Security', + }, + ], + workloadAccounts: [], + accountIds: [ + { + email: 'audit-account@amazon.com', + accountId: '222222222222', + }, + { + email: 'logarchive-account@amazon.com', + accountId: '111111111111', + }, + { email: 'management-account@amazon.com', accountId: '333333333333' }, + ], + // eslint-disable-next-line @typescript-eslint/no-empty-function + loadAccountIds: async function (): Promise {}, + containsAccount: function (name: string) { + return !!name; + }, + getManagementAccount: function () { + return { + name: 'Management', + description: 'The management (primary) account', + email: 'management-account@amazon.com', + organizationalUnit: 'Root', + }; + }, + getLogArchiveAccount: function () { + return { + name: 'LogArchive', + description: 'The log archive account', + email: 'logarchive-account@amazon.com', + organizationalUnit: 'Security', + }; + }, + getAuditAccount: function () { + return { + name: 'Audit', + description: 'The security audit account (also referred to as the audit account)', + email: 'audit-account@amazon.com', + organizationalUnit: 'Security', + }; + }, + getAccount: function (name: string) { + return { + name: name, + description: 'The management (primary) account', + email: 'management-account@amazon.com', + organizationalUnit: 'Root', + }; + }, + getAuditAccountId: function () { + return '222222222222'; + }, + getManagementAccountId: function () { + return '333333333333'; + }, + getLogArchiveAccountId: function () { + return '111111111111'; + }, + getAccountId: function (name: string) { + if (name === 'Management') { + return '333333333333'; + } + if (name === 'Audit') { + return '222222222222'; + } + return '111111111111'; + }, + isGovCloudAccount: function () { + return false; + }, + anyGovCloudAccounts: function () { + return false; + }, + isGovCloudEnabled: function () { + return false; + }, +}; diff --git a/source/packages/@aws-accelerator/accelerator/test/configs/vpc-endpoint-policies/default.json b/source/packages/@aws-accelerator/accelerator/test/configs/vpc-endpoint-policies/default.json new file mode 100644 index 000000000..a812fa1f3 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/test/configs/vpc-endpoint-policies/default.json @@ -0,0 +1,10 @@ +{ + "Statement": [ + { + "Effect": "Allow", + "Principal": "*", + "Action": "*", + "Resource": "*" + } + ] +} \ No newline at end of file diff --git a/source/packages/@aws-accelerator/accelerator/test/finalize-stack.test.ts b/source/packages/@aws-accelerator/accelerator/test/finalize-stack.test.ts new file mode 100644 index 000000000..c224bd1ac --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/test/finalize-stack.test.ts @@ -0,0 +1,121 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; + +import { FinalizeStack } from '../lib/stacks/finalize-stack'; +import { AcceleratorStackNames } from '../lib/accelerator'; +import { AcceleratorStage } from '../lib/accelerator-stage'; +import { + ACCOUNT_CONFIG, + GLOBAL_CONFIG, + IAM_CONFIG, + NETWORK_CONFIG, + ORGANIZATION_CONFIG, + SECURITY_CONFIG, +} from './configs/test-config'; +import * as path from 'path'; +import { AcceleratorStackProps } from '../lib/stacks/accelerator-stack'; +//import { SynthUtils } from '@aws-cdk/assert'; + +const testNamePrefix = 'Construct(FinalizeStack): '; + +/** + * Finalize Stack + */ +const app = new cdk.App({ + context: { 'config-dir': path.join(__dirname, 'configs') }, +}); +const configDirPath = app.node.tryGetContext('config-dir'); + +const env = { + account: '333333333333', + region: 'us-east-1', +}; + +const props: AcceleratorStackProps = { + env, + configDirPath, + accountsConfig: ACCOUNT_CONFIG, + globalConfig: GLOBAL_CONFIG, + iamConfig: IAM_CONFIG, + networkConfig: NETWORK_CONFIG, + organizationConfig: ORGANIZATION_CONFIG, + securityConfig: SECURITY_CONFIG, + partition: 'aws', +}; + +const stack = new FinalizeStack( + app, + `${AcceleratorStackNames[AcceleratorStage.ACCOUNTS]}-${env.account}-${env.region}`, + props, +); + +/** + * FinalizeStack construct test + */ +describe('FinalizeStack', () => { + // /** + // * Snapshot test + // */ + //test(`${testNamePrefix} Snapshot Test`, () => { + // expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); + //}); + + /** + * Number of SSM parameters resource test + */ + test(`${testNamePrefix} SSM parameters resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::SSM::Parameter', 2); + }); + + /** + * Number of IAM Role resource test + */ + test(`${testNamePrefix} IAM Role resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 1); + }); + + /** + * Number of Lambda Function resource test + */ + test(`${testNamePrefix} Lambda Function resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 1); + }); + + /** + * Number of Cloudwatch Log groups test + */ + test(`${testNamePrefix} CloudWatch Log Group resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Logs::LogGroup', 1); + }); + + test(`${testNamePrefix} Detach Quarantinee custom resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomDetachQuarantineScpCustomResourceProviderHandlerA1F1C263: { + Type: 'AWS::Lambda::Function', + Properties: { + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomDetachQuarantineScpCustomResourceProviderRoleE5C433C1', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/accelerator/test/logging-stack.test.ts b/source/packages/@aws-accelerator/accelerator/test/logging-stack.test.ts new file mode 100644 index 000000000..a395e1b47 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/test/logging-stack.test.ts @@ -0,0 +1,428 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { LoggingStack } from '../lib/stacks/logging-stack'; +import { AcceleratorStackNames } from '../lib/accelerator'; +import { AcceleratorStage } from '../lib/accelerator-stage'; +import { + ACCOUNT_CONFIG, + GLOBAL_CONFIG, + IAM_CONFIG, + NETWORK_CONFIG, + ORGANIZATION_CONFIG, + SECURITY_CONFIG, +} from './configs/test-config'; +import * as path from 'path'; +import { AcceleratorStackProps } from '../lib/stacks/accelerator-stack'; + +const testNamePrefix = 'Construct(LoggingStack): '; + +/** + * LoggingStack + */ +const app = new cdk.App({ + context: { 'config-dir': path.join(__dirname, 'configs') }, +}); +const configDirPath = app.node.tryGetContext('config-dir'); + +const env = { + account: '333333333333', + region: 'us-east-1', +}; + +const props: AcceleratorStackProps = { + env, + configDirPath, + accountsConfig: ACCOUNT_CONFIG, + globalConfig: GLOBAL_CONFIG, + iamConfig: IAM_CONFIG, + networkConfig: NETWORK_CONFIG, + organizationConfig: ORGANIZATION_CONFIG, + securityConfig: SECURITY_CONFIG, + partition: 'aws', +}; +console.log(props); +const stack = new LoggingStack( + app, + `${AcceleratorStackNames[AcceleratorStage.LOGGING]}-${env.account}-${env.region}`, + props, +); + +/** + * LoggingStack construct test + */ +describe('LoggingStack', () => { + /** + * Number of S3 Bucket resource test + */ + test(`${testNamePrefix} S3 Bucket resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::S3::Bucket', 1); + }); + + /** + * Number of BucketPolicy resource test + */ + test(`${testNamePrefix} BucketPolicy resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::S3::BucketPolicy', 1); + }); + + /** + * Number of Lambda Function resource test + */ + test(`${testNamePrefix} Lambda Function resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 3); + }); + + /** + * Number of IAM Role resource test + */ + test(`${testNamePrefix} IAM Role resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 3); + }); + + /** + * Number of DescribeOrganization custom resource test + */ + test(`${testNamePrefix} DescribeOrganization custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::DescribeOrganization', 1); + }); + + /** + * Number of PutPublicAccessBlock custom resource test + */ + test(`${testNamePrefix} PutPublicAccessBlock custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::PutPublicAccessBlock', 1); + }); + + /** + * AccessLogsBucket resource configuration test + */ + test(`${testNamePrefix} AccessLogsBucket resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + AccessLogsBucketFA218D2A: { + Type: 'AWS::S3::Bucket', + UpdateReplacePolicy: 'Retain', + DeletionPolicy: 'Retain', + Metadata: { + cdk_nag: { + rules_to_suppress: [ + { + id: 'AwsSolutions-S1', + reason: + 'AccessLogsBucket has server access logs disabled till the task for access logging completed.', + }, + ], + }, + }, + Properties: { + BucketEncryption: { + ServerSideEncryptionConfiguration: [ + { + ServerSideEncryptionByDefault: { + SSEAlgorithm: 'AES256', + }, + }, + ], + }, + BucketName: 'aws-accelerator-s3-access-logs-333333333333-us-east-1', + OwnershipControls: { + Rules: [ + { + ObjectOwnership: 'BucketOwnerPreferred', + }, + ], + }, + PublicAccessBlockConfiguration: { + BlockPublicAcls: true, + BlockPublicPolicy: true, + IgnorePublicAcls: true, + RestrictPublicBuckets: true, + }, + VersioningConfiguration: { + Status: 'Enabled', + }, + }, + }, + }, + }); + }); + + /** + * AccessLogsBucketPolicy resource configuration test + */ + test(`${testNamePrefix} AccessLogsBucketPolicy resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + AccessLogsBucketPolicy00F12803: { + Type: 'AWS::S3::BucketPolicy', + Properties: { + Bucket: { + Ref: 'AccessLogsBucketFA218D2A', + }, + PolicyDocument: { + Statement: [ + { + Action: 's3:*', + Condition: { + Bool: { + 'aws:SecureTransport': 'false', + }, + }, + Effect: 'Deny', + Principal: { + AWS: '*', + }, + Resource: [ + { + 'Fn::GetAtt': ['AccessLogsBucketFA218D2A', 'Arn'], + }, + { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': ['AccessLogsBucketFA218D2A', 'Arn'], + }, + '/*', + ], + ], + }, + ], + Sid: 'deny-insecure-connections', + }, + { + Action: 's3:PutObject', + Condition: { + StringEquals: { + 'aws:SourceAccount': '333333333333', + }, + }, + Effect: 'Allow', + Principal: { + Service: 'logging.s3.amazonaws.com', + }, + Resource: { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': ['AccessLogsBucketFA218D2A', 'Arn'], + }, + '/*', + ], + ], + }, + Sid: 'Allow write access for logging service principal', + }, + ], + Version: '2012-10-17', + }, + }, + }, + }, + }); + }); + + /** + * CustomOrganizationsDescribeOrganizationCustomResourceProviderHandler resource configuration test + */ + test(`${testNamePrefix} CustomOrganizationsDescribeOrganizationCustomResourceProviderHandler resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomOrganizationsDescribeOrganizationCustomResourceProviderHandler4C6F49D1: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomOrganizationsDescribeOrganizationCustomResourceProviderRole775854D5'], + Properties: { + Code: { + S3Bucket: 'cdk-hnb659fds-assets-333333333333-us-east-1', + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomOrganizationsDescribeOrganizationCustomResourceProviderRole775854D5', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * CustomOrganizationsDescribeOrganizationCustomResourceProviderRole resource configuration test + */ + test(`${testNamePrefix} CustomOrganizationsDescribeOrganizationCustomResourceProviderRole resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomOrganizationsDescribeOrganizationCustomResourceProviderRole775854D5: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: ['organizations:DescribeOrganization'], + Effect: 'Allow', + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * CustomS3PutPublicAccessBlockCustomResourceProviderHandler resource configuration test + */ + test(`${testNamePrefix} CustomS3PutPublicAccessBlockCustomResourceProviderHandler resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomS3PutPublicAccessBlockCustomResourceProviderHandler978E227B: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomS3PutPublicAccessBlockCustomResourceProviderRole656EB36E'], + Properties: { + Code: { + S3Bucket: 'cdk-hnb659fds-assets-333333333333-us-east-1', + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomS3PutPublicAccessBlockCustomResourceProviderRole656EB36E', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * CustomS3PutPublicAccessBlockCustomResourceProviderRole resource configuration test + */ + test(`${testNamePrefix} CustomS3PutPublicAccessBlockCustomResourceProviderRole resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomS3PutPublicAccessBlockCustomResourceProviderRole656EB36E: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: ['s3:PutAccountPublicAccessBlock'], + Effect: 'Allow', + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * Organization custom resource configuration test + */ + test(`${testNamePrefix} Organization custom resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + Organization29A5FC3F: { + Type: 'Custom::DescribeOrganization', + UpdateReplacePolicy: 'Delete', + DeletionPolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomOrganizationsDescribeOrganizationCustomResourceProviderHandler4C6F49D1', 'Arn'], + }, + }, + }, + }, + }); + }); + + /** + * S3PublicAccessBlock custom resource configuration test + */ + test(`${testNamePrefix} S3PublicAccessBlock custom resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + S3PublicAccessBlock344F906B: { + Type: 'Custom::PutPublicAccessBlock', + UpdateReplacePolicy: 'Delete', + DeletionPolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomS3PutPublicAccessBlockCustomResourceProviderHandler978E227B', 'Arn'], + }, + accountId: '333333333333', + blockPublicAcls: true, + blockPublicPolicy: true, + ignorePublicAcls: true, + restrictPublicBuckets: true, + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/accelerator/test/network-associations-stack.test.ts b/source/packages/@aws-accelerator/accelerator/test/network-associations-stack.test.ts new file mode 100644 index 000000000..197f62121 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/test/network-associations-stack.test.ts @@ -0,0 +1,355 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import * as path from 'path'; + +import { AcceleratorStackNames } from '../lib/accelerator'; +import { AcceleratorStage } from '../lib/accelerator-stage'; +import { AcceleratorStackProps } from '../lib/stacks/accelerator-stack'; +import { NetworkAssociationsStack } from '../lib/stacks/network-associations-stack'; +import { + ACCOUNT_CONFIG, + GLOBAL_CONFIG, + IAM_CONFIG, + NETWORK_CONFIG, + ORGANIZATION_CONFIG, + SECURITY_CONFIG, +} from './configs/test-config'; + +const testNamePrefix = 'Construct(NetworkAssociationsStack): '; + +/** + * NetworkAssociationsStack + */ +const app = new cdk.App({ + context: { 'config-dir': path.join(__dirname, 'configs') }, +}); +const configDirPath = app.node.tryGetContext('config-dir'); + +const env = { + account: '333333333333', + region: 'us-east-1', +}; + +const props: AcceleratorStackProps = { + env, + configDirPath, + accountsConfig: ACCOUNT_CONFIG, + globalConfig: GLOBAL_CONFIG, + iamConfig: IAM_CONFIG, + networkConfig: NETWORK_CONFIG, + organizationConfig: ORGANIZATION_CONFIG, + securityConfig: SECURITY_CONFIG, + partition: 'aws', +}; + +const stack = new NetworkAssociationsStack( + app, + `${AcceleratorStackNames[AcceleratorStage.NETWORK_ASSOCIATIONS]}-${env.account}-${env.region}`, + props, +); + +/** + * NetworkAssociationsStack construct test + */ +describe('NetworkAssociationsStack', () => { + /** + * Number of Lambda function resource test + */ + test(`${testNamePrefix} Lambda function resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 2); + }); + + /** + * Number of Lambda function IAM role resource test + */ + test(`${testNamePrefix} Lambda function IAM role resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 2); + }); + + /** + * Number of SSM parameter resource test + */ + test(`${testNamePrefix} SSM parameter resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::SSM::Parameter', 2); + }); + + /** + * Number of TransitGatewayRouteTablePropagation resource test + */ + test(`${testNamePrefix} TransitGatewayRouteTablePropagation resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::EC2::TransitGatewayRouteTablePropagation', 3); + }); + + /** + * Number of TransitGatewayRouteTableAssociation resource test + */ + test(`${testNamePrefix} TransitGatewayRouteTablePropagation resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::EC2::TransitGatewayRouteTableAssociation', 1); + }); + + /** + * Number of GetTransitGatewayAttachment custom resource test + */ + test(`${testNamePrefix} GetTransitGatewayAttachment custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::GetTransitGatewayAttachment', 1); + }); + + /** + * Cloudformation parameters resource configuration test + */ + test(`${testNamePrefix} Cloudformation parameters resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Parameters: { + SsmParameterValueacceleratornetworktransitGatewaysMainidC96584B6F00A464EAD1953AFF4B05118Parameter: { + Default: '/accelerator/network/transitGateways/Main/id', + Type: 'AWS::SSM::Parameter::Value', + }, + SsmParameterValueacceleratornetworktransitGatewaysMainrouteTablescoreidC96584B6F00A464EAD1953AFF4B05118Parameter: + { + Default: '/accelerator/network/transitGateways/Main/routeTables/core/id', + Type: 'AWS::SSM::Parameter::Value', + }, + SsmParameterValueacceleratornetworktransitGatewaysMainrouteTablessegregatedidC96584B6F00A464EAD1953AFF4B05118Parameter: + { + Default: '/accelerator/network/transitGateways/Main/routeTables/segregated/id', + Type: 'AWS::SSM::Parameter::Value', + }, + SsmParameterValueacceleratornetworktransitGatewaysMainrouteTablessharedidC96584B6F00A464EAD1953AFF4B05118Parameter: + { + Default: '/accelerator/network/transitGateways/Main/routeTables/shared/id', + Type: 'AWS::SSM::Parameter::Value', + }, + SsmParameterValueacceleratornetworktransitGatewaysMainrouteTablesstandaloneidC96584B6F00A464EAD1953AFF4B05118Parameter: + { + Default: '/accelerator/network/transitGateways/Main/routeTables/standalone/id', + Type: 'AWS::SSM::Parameter::Value', + }, + }, + }); + }); + + /** + * Lambda function CustomGetTransitGatewayAttachmentCustomResourceProviderHandler resource configuration test + */ + test(`${testNamePrefix} Lambda function CustomGetTransitGatewayAttachmentCustomResourceProviderHandler resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomGetTransitGatewayAttachmentCustomResourceProviderHandler7E079354: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomGetTransitGatewayAttachmentCustomResourceProviderRoleA6A22C3D'], + Properties: { + Code: { + S3Bucket: 'cdk-hnb659fds-assets-333333333333-us-east-1', + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomGetTransitGatewayAttachmentCustomResourceProviderRoleA6A22C3D', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * Lambda function IAM role CustomGetTransitGatewayAttachmentCustomResourceProviderRole resource configuration test + */ + test(`${testNamePrefix} Lambda function IAM role CustomGetTransitGatewayAttachmentCustomResourceProviderRole resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomGetTransitGatewayAttachmentCustomResourceProviderRoleA6A22C3D: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: ['sts:AssumeRole'], + Effect: 'Allow', + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * SSM parameter SsmParamStackId resource configuration test + */ + test(`${testNamePrefix} SSM parameter SsmParamStackId resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + SsmParamStackId521A78D3: { + Type: 'AWS::SSM::Parameter', + Properties: { + Name: '/accelerator/AWSAccelerator-NetworkAssociationsStack-333333333333-us-east-1/stack-id', + Type: 'String', + Value: { + Ref: 'AWS::StackId', + }, + }, + }, + }, + }); + }); + + /** + * TransitGatewayRouteTablePropagation TestCorePropagation resource configuration test + */ + test(`${testNamePrefix} TransitGatewayRouteTablePropagation TestCorePropagation resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TestCorePropagationB97A6DBD: { + Type: 'AWS::EC2::TransitGatewayRouteTablePropagation', + Properties: { + TransitGatewayAttachmentId: { + Ref: 'TestVpcTransitGatewayAttachmentA903FB56', + }, + TransitGatewayRouteTableId: { + Ref: 'SsmParameterValueacceleratornetworktransitGatewaysMainrouteTablescoreidC96584B6F00A464EAD1953AFF4B05118Parameter', + }, + }, + }, + }, + }); + }); + + /** + * TransitGatewayRouteTablePropagation TestSegregatedPropagation resource configuration test + */ + test(`${testNamePrefix} TransitGatewayRouteTablePropagation TestSegregatedPropagation resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TestSegregatedPropagationCA3F8CD1: { + Type: 'AWS::EC2::TransitGatewayRouteTablePropagation', + Properties: { + TransitGatewayAttachmentId: { + Ref: 'TestVpcTransitGatewayAttachmentA903FB56', + }, + TransitGatewayRouteTableId: { + Ref: 'SsmParameterValueacceleratornetworktransitGatewaysMainrouteTablessegregatedidC96584B6F00A464EAD1953AFF4B05118Parameter', + }, + }, + }, + }, + }); + }); + + /** + * TransitGatewayRouteTableAssociation TestSharedAssociation resource configuration test + */ + test(`${testNamePrefix} TransitGatewayRouteTableAssociation TestSharedAssociation resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TestSharedAssociation1890469B: { + Type: 'AWS::EC2::TransitGatewayRouteTableAssociation', + Properties: { + TransitGatewayAttachmentId: { + Ref: 'TestVpcTransitGatewayAttachmentA903FB56', + }, + TransitGatewayRouteTableId: { + Ref: 'SsmParameterValueacceleratornetworktransitGatewaysMainrouteTablessharedidC96584B6F00A464EAD1953AFF4B05118Parameter', + }, + }, + }, + }, + }); + }); + + /** + * TransitGatewayRouteTablePropagation TestSharedPropagation resource configuration test + */ + test(`${testNamePrefix} TransitGatewayRouteTablePropagation TestSharedPropagation resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TestSharedPropagation66A144ED: { + Type: 'AWS::EC2::TransitGatewayRouteTablePropagation', + Properties: { + TransitGatewayAttachmentId: { + Ref: 'TestVpcTransitGatewayAttachmentA903FB56', + }, + TransitGatewayRouteTableId: { + Ref: 'SsmParameterValueacceleratornetworktransitGatewaysMainrouteTablessharedidC96584B6F00A464EAD1953AFF4B05118Parameter', + }, + }, + }, + }, + }); + }); + + /** + * GetTransitGatewayAttachment TestVpcTransitGatewayAttachment resource configuration test + */ + test(`${testNamePrefix} GetTransitGatewayAttachment TestVpcTransitGatewayAttachment resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TestVpcTransitGatewayAttachmentA903FB56: { + Type: 'Custom::GetTransitGatewayAttachment', + UpdateReplacePolicy: 'Delete', + DeletionPolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomGetTransitGatewayAttachmentCustomResourceProviderHandler7E079354', 'Arn'], + }, + name: 'Test', + roleArn: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::222222222222:role/AWSAccelerator-DescribeTgwAttachRole-us-east-1', + ], + ], + }, + transitGatewayId: { + Ref: 'SsmParameterValueacceleratornetworktransitGatewaysMainidC96584B6F00A464EAD1953AFF4B05118Parameter', + }, + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/accelerator/test/network-prep-stack.test.ts b/source/packages/@aws-accelerator/accelerator/test/network-prep-stack.test.ts new file mode 100644 index 000000000..afe7225d2 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/test/network-prep-stack.test.ts @@ -0,0 +1,371 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import * as path from 'path'; + +import { AcceleratorStackNames } from '../lib/accelerator'; +import { AcceleratorStage } from '../lib/accelerator-stage'; +import { AcceleratorStackProps } from '../lib/stacks/accelerator-stack'; +import { NetworkPrepStack } from '../lib/stacks/network-prep-stack'; +import { + ACCOUNT_CONFIG, + GLOBAL_CONFIG, + IAM_CONFIG, + NETWORK_CONFIG, + ORGANIZATION_CONFIG, + SECURITY_CONFIG, +} from './configs/test-config'; + +const testNamePrefix = 'Construct(NetworkPrepStack): '; + +/** + * NetworkPrepStack + */ +const app = new cdk.App({ + context: { 'config-dir': path.join(__dirname, 'configs') }, +}); +const configDirPath = app.node.tryGetContext('config-dir'); + +const env = { + account: '333333333333', + region: 'us-east-1', +}; + +const props: AcceleratorStackProps = { + env, + configDirPath, + accountsConfig: ACCOUNT_CONFIG, + globalConfig: GLOBAL_CONFIG, + iamConfig: IAM_CONFIG, + networkConfig: NETWORK_CONFIG, + organizationConfig: ORGANIZATION_CONFIG, + securityConfig: SECURITY_CONFIG, + partition: 'aws', +}; + +const stack = new NetworkPrepStack( + app, + `${AcceleratorStackNames[AcceleratorStage.NETWORK_PREP]}-${env.account}-${env.region}`, + props, +); + +/** + * NetworkPrepStack construct test + */ +describe('NetworkPrepStack', () => { + /** + * Number of TransitGatewayRouteTable resource test + */ + test(`${testNamePrefix} TransitGatewayRouteTable resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::EC2::TransitGatewayRouteTable', 4); + }); + + /** + * Number of TransitGateway resource test + */ + test(`${testNamePrefix} TransitGateway resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::EC2::TransitGateway', 1); + }); + + /** + * Number of RAM ResourceShare resource test + */ + test(`${testNamePrefix} RAM ResourceShare resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::RAM::ResourceShare', 1); + }); + + /** + * Number of SSM parameter resource test + */ + test(`${testNamePrefix} SSM parameter resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::SSM::Parameter', 7); + }); + + /** + * CoreTransitGatewayRouteTable TransitGatewayRouteTable resource configuration test + */ + test(`${testNamePrefix} CoreTransitGatewayRouteTable TransitGatewayRouteTable resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CoreTransitGatewayRouteTableD73AD6A9: { + Type: 'AWS::EC2::TransitGatewayRouteTable', + Properties: { + Tags: [ + { + Key: 'Name', + Value: 'core', + }, + ], + TransitGatewayId: { + Ref: 'MainTransitGateway66204EF2', + }, + }, + }, + }, + }); + }); + + /** + * MainTransitGateway TransitGateway resource configuration test + */ + test(`${testNamePrefix} MainTransitGateway TransitGateway resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + MainTransitGateway66204EF2: { + Type: 'AWS::EC2::TransitGateway', + Properties: { + AmazonSideAsn: 65521, + AutoAcceptSharedAttachments: 'enable', + DefaultRouteTableAssociation: 'disable', + DefaultRouteTablePropagation: 'disable', + DnsSupport: 'enable', + Tags: [ + { + Key: 'Name', + Value: 'Main', + }, + ], + VpnEcmpSupport: 'enable', + }, + }, + }, + }); + }); + + /** + * RAM ResourceShare MainTransitGatewayShareMainTransitGatewayShareResourceShare resource configuration test + */ + test(`${testNamePrefix} RAM ResourceShare MainTransitGatewayShareMainTransitGatewayShareResourceShare resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + MainTransitGatewayShareResourceShare02109087: { + Type: 'AWS::RAM::ResourceShare', + Properties: { + Name: 'Main_TransitGatewayShare', + Principals: ['Sandbox-arn', '222222222222'], + ResourceArns: [ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':ec2:us-east-1:333333333333:transit-gateway/', + { + Ref: 'MainTransitGateway66204EF2', + }, + ], + ], + }, + ], + }, + }, + }, + }); + }); + + /** + * CoreTransitGatewayRouteTable SegregatedTransitGatewayRouteTable resource configuration test + */ + test(`${testNamePrefix} CoreTransitGatewayRouteTable SegregatedTransitGatewayRouteTable resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + SegregatedTransitGatewayRouteTableFBA11CE4: { + Type: 'AWS::EC2::TransitGatewayRouteTable', + Properties: { + Tags: [ + { + Key: 'Name', + Value: 'segregated', + }, + ], + TransitGatewayId: { + Ref: 'MainTransitGateway66204EF2', + }, + }, + }, + }, + }); + }); + + /** + * CoreTransitGatewayRouteTable SharedTransitGatewayRouteTable resource configuration test + */ + test(`${testNamePrefix} CoreTransitGatewayRouteTable SharedTransitGatewayRouteTable resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + SharedTransitGatewayRouteTableDEC04AD4: { + Type: 'AWS::EC2::TransitGatewayRouteTable', + Properties: { + Tags: [ + { + Key: 'Name', + Value: 'shared', + }, + ], + TransitGatewayId: { + Ref: 'MainTransitGateway66204EF2', + }, + }, + }, + }, + }); + }); + + /** + * SSM parameter SsmParamMainTransitGatewayId resource configuration test + */ + test(`${testNamePrefix} SSM parameter SsmParamMainTransitGatewayId resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + SsmParamMainTransitGatewayId76D30719: { + Type: 'AWS::SSM::Parameter', + Properties: { + Name: '/accelerator/network/transitGateways/Main/id', + Type: 'String', + Value: { + Ref: 'MainTransitGateway66204EF2', + }, + }, + }, + }, + }); + }); + + /** + * CoreTransitGatewayRouteTable StandaloneTransitGatewayRouteTable resource configuration test + */ + test(`${testNamePrefix} CoreTransitGatewayRouteTable StandaloneTransitGatewayRouteTable resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + StandaloneTransitGatewayRouteTableD8B42C98: { + Type: 'AWS::EC2::TransitGatewayRouteTable', + Properties: { + Tags: [ + { + Key: 'Name', + Value: 'standalone', + }, + ], + TransitGatewayId: { + Ref: 'MainTransitGateway66204EF2', + }, + }, + }, + }, + }); + }); + + /** + * SSM parameter SsmParamMaincoreTransitGatewayRouteTableId resource configuration test + */ + test(`${testNamePrefix} SSM parameter SsmParamMaincoreTransitGatewayRouteTableId resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + SsmParamMaincoreTransitGatewayRouteTableIdC4F7B376: { + Type: 'AWS::SSM::Parameter', + Properties: { + Name: '/accelerator/network/transitGateways/Main/routeTables/core/id', + Type: 'String', + Value: { + Ref: 'CoreTransitGatewayRouteTableD73AD6A9', + }, + }, + }, + }, + }); + }); + + /** + * SSM parameter SsmParamMainsegregatedTransitGatewayRouteTableId resource configuration test + */ + test(`${testNamePrefix} SSM parameter SsmParamMainsegregatedTransitGatewayRouteTableId resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + SsmParamMainsegregatedTransitGatewayRouteTableId8DCAFE8D: { + Type: 'AWS::SSM::Parameter', + Properties: { + Name: '/accelerator/network/transitGateways/Main/routeTables/segregated/id', + Type: 'String', + Value: { + Ref: 'SegregatedTransitGatewayRouteTableFBA11CE4', + }, + }, + }, + }, + }); + }); + + /** + * SSM parameter SsmParamMainsharedTransitGatewayRouteTableId resource configuration test + */ + test(`${testNamePrefix} SSM parameter SsmParamMainsharedTransitGatewayRouteTableId resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + SsmParamMainsharedTransitGatewayRouteTableId2B981DF1: { + Type: 'AWS::SSM::Parameter', + Properties: { + Name: '/accelerator/network/transitGateways/Main/routeTables/shared/id', + Type: 'String', + Value: { + Ref: 'SharedTransitGatewayRouteTableDEC04AD4', + }, + }, + }, + }, + }); + }); + + /** + * SSM parameter SsmParamMainstandaloneTransitGatewayRouteTableId resource configuration test + */ + test(`${testNamePrefix} SSM parameter SsmParamMainstandaloneTransitGatewayRouteTableId resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + SsmParamMainstandaloneTransitGatewayRouteTableIdE6B97388: { + Type: 'AWS::SSM::Parameter', + Properties: { + Name: '/accelerator/network/transitGateways/Main/routeTables/standalone/id', + Type: 'String', + Value: { + Ref: 'StandaloneTransitGatewayRouteTableD8B42C98', + }, + }, + }, + }, + }); + }); + + /** + * SSM parameter SsmParamMainTransitGatewayId resource configuration test + */ + test(`${testNamePrefix} SSM parameter SsmParamStackId resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + SsmParamStackId521A78D3: { + Type: 'AWS::SSM::Parameter', + Properties: { + Name: '/accelerator/AWSAccelerator-NetworkPrepStack-333333333333-us-east-1/stack-id', + Type: 'String', + Value: { + Ref: 'AWS::StackId', + }, + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/accelerator/test/network-vpc-dns-stack.test.ts b/source/packages/@aws-accelerator/accelerator/test/network-vpc-dns-stack.test.ts new file mode 100644 index 000000000..d780e0944 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/test/network-vpc-dns-stack.test.ts @@ -0,0 +1,258 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import * as path from 'path'; + +import { AcceleratorStackNames } from '../lib/accelerator'; +import { AcceleratorStage } from '../lib/accelerator-stage'; +import { AcceleratorStackProps } from '../lib/stacks/accelerator-stack'; +import { NetworkVpcDnsStack } from '../lib/stacks/network-vpc-dns-stack'; +import { + ACCOUNT_CONFIG, + GLOBAL_CONFIG, + IAM_CONFIG, + NETWORK_CONFIG, + ORGANIZATION_CONFIG, + SECURITY_CONFIG, +} from './configs/test-config'; + +const testNamePrefix = 'Construct(NetworkVpcDnsStack): '; + +/** + * NetworkVpcEndpointsStack + */ +const app = new cdk.App({ + context: { 'config-dir': path.join(__dirname, 'configs') }, +}); +const configDirPath = app.node.tryGetContext('config-dir'); + +const env = { + account: '333333333333', + region: 'us-east-1', +}; + +const props: AcceleratorStackProps = { + env, + configDirPath, + accountsConfig: ACCOUNT_CONFIG, + globalConfig: GLOBAL_CONFIG, + iamConfig: IAM_CONFIG, + networkConfig: NETWORK_CONFIG, + organizationConfig: ORGANIZATION_CONFIG, + securityConfig: SECURITY_CONFIG, + partition: 'aws', +}; + +const stack = new NetworkVpcDnsStack( + app, + `${AcceleratorStackNames[AcceleratorStage.NETWORK_VPC_DNS]}-${env.account}-${env.region}`, + props, +); + +/** + * NetworkVpcDnsStack construct test + */ +describe('NetworkVpcDnsStack', () => { + /** + * Number of SsmGetParameterValue custom resource test + */ + test(`${testNamePrefix} SsmGetParameterValue custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::SsmGetParameterValue', 1); + }); + + /** + * Number of Lambda function resource test + */ + test(`${testNamePrefix} Lambda function resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 1); + }); + + /** + * Number of Lambda function IAM role resource test + */ + test(`${testNamePrefix} Lambda function IAM role resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 1); + }); + + /** + * Number of SSM parameter resource test + */ + test(`${testNamePrefix} SSM parameter custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::SSM::Parameter', 2); + }); + + /** + * CustomSsmGetParameterValue resource configuration test + */ + test(`${testNamePrefix} CustomSsmGetParameterValue resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + AcceleratorKeyLookup0C18DA36: { + Type: 'Custom::SsmGetParameterValue', + DependsOn: ['CustomSsmGetParameterValueCustomResourceProviderLogGroup780D220D'], + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomSsmGetParameterValueCustomResourceProviderHandlerAAD0E7EE', 'Arn'], + }, + assumeRoleArn: { + 'Fn::Join': [ + '', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::222222222222:role/AWSAccelerator-CrossAccount-SsmParameter-Role', + ], + ], + }, + invokingAccountID: '333333333333', + parameterAccountID: '222222222222', + parameterName: '/accelerator/kms/key-arn', + region: 'us-east-1', + }, + }, + }, + }); + }); + + /** + * Lambda function IAM role resource configuration test + */ + test(`${testNamePrefix} Lambda function IAM role CustomSsmGetParameterValue resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomSsmGetParameterValueCustomResourceProviderRoleB3AFDDB2: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: ['ssm:GetParameters', 'ssm:GetParameter', 'ssm:DescribeParameters'], + Effect: 'Allow', + Resource: ['*'], + }, + { + Action: ['sts:AssumeRole'], + Effect: 'Allow', + Resource: [ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::222222222222:role/AWSAccelerator-CrossAccount-SsmParameter-Role', + ], + ], + }, + ], + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * Lambda function resource configuration test + */ + test(`${testNamePrefix} Lambda function resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomSsmGetParameterValueCustomResourceProviderHandlerAAD0E7EE: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomSsmGetParameterValueCustomResourceProviderRoleB3AFDDB2'], + Properties: { + Code: { + S3Bucket: 'cdk-hnb659fds-assets-333333333333-us-east-1', + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomSsmGetParameterValueCustomResourceProviderRoleB3AFDDB2', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * CloudWatch log group resource configuration test + */ + test(`${testNamePrefix} CloudWatch log group resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomSsmGetParameterValueCustomResourceProviderLogGroup780D220D: { + Type: 'AWS::Logs::LogGroup', + Properties: { + LogGroupName: { + 'Fn::Join': [ + '', + ['/aws/lambda/', { Ref: 'CustomSsmGetParameterValueCustomResourceProviderHandlerAAD0E7EE' }], + ], + }, + RetentionInDays: 3653, + }, + }, + }, + }); + }); + + /** + * SSM parameter SsmParamStackId resource configuration test + */ + test(`${testNamePrefix} SSM parameter SsmParamStackId resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + SsmParamStackId521A78D3: { + Type: 'AWS::SSM::Parameter', + Properties: { + Name: '/accelerator/AWSAccelerator-NetworkVpcDnsStack-333333333333-us-east-1/stack-id', + Type: 'String', + Value: { + Ref: 'AWS::StackId', + }, + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/accelerator/test/network-vpc-endpoints-stack.test.ts b/source/packages/@aws-accelerator/accelerator/test/network-vpc-endpoints-stack.test.ts new file mode 100644 index 000000000..d66722b29 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/test/network-vpc-endpoints-stack.test.ts @@ -0,0 +1,257 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import * as path from 'path'; + +import { AcceleratorStackNames } from '../lib/accelerator'; +import { AcceleratorStage } from '../lib/accelerator-stage'; +import { AcceleratorStackProps } from '../lib/stacks/accelerator-stack'; +import { NetworkVpcEndpointsStack } from '../lib/stacks/network-vpc-endpoints-stack'; +import { + ACCOUNT_CONFIG, + GLOBAL_CONFIG, + IAM_CONFIG, + NETWORK_CONFIG, + ORGANIZATION_CONFIG, + SECURITY_CONFIG, +} from './configs/test-config'; + +const testNamePrefix = 'Construct(NetworkVpcEndpointsStack): '; + +/** + * NetworkVpcEndpointsStack + */ +const app = new cdk.App({ + context: { 'config-dir': path.join(__dirname, 'configs') }, +}); +const configDirPath = app.node.tryGetContext('config-dir'); + +const env = { + account: '333333333333', + region: 'us-east-1', +}; + +const props: AcceleratorStackProps = { + env, + configDirPath, + accountsConfig: ACCOUNT_CONFIG, + globalConfig: GLOBAL_CONFIG, + iamConfig: IAM_CONFIG, + networkConfig: NETWORK_CONFIG, + organizationConfig: ORGANIZATION_CONFIG, + securityConfig: SECURITY_CONFIG, + partition: 'aws', +}; + +const stack = new NetworkVpcEndpointsStack( + app, + `${AcceleratorStackNames[AcceleratorStage.NETWORK_VPC_ENDPOINTS]}-${env.account}-${env.region}`, + props, +); + +/** + * NetworkVpcStack construct test + */ +describe('NetworkVpcEndpointsStack', () => { + /** + * Number of SsmGetParameterValue custom resource test + */ + test(`${testNamePrefix} SsmGetParameterValue custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::SsmGetParameterValue', 1); + }); + + /** + * Number of Lambda function resource test + */ + test(`${testNamePrefix} Lambda function resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 1); + }); + + /** + * Number of Lambda function IAM role resource test + */ + test(`${testNamePrefix} Lambda function IAM role resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 1); + }); + + /** + * Number of SSM parameter resource test + */ + test(`${testNamePrefix} SSM parameter custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::SSM::Parameter', 2); + }); + + /** + * CustomSsmGetParameterValue resource configuration test + */ + test(`${testNamePrefix} CustomSsmGetParameterValue resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + AcceleratorKeyLookup0C18DA36: { + Type: 'Custom::SsmGetParameterValue', + DependsOn: ['CustomSsmGetParameterValueCustomResourceProviderLogGroup780D220D'], + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomSsmGetParameterValueCustomResourceProviderHandlerAAD0E7EE', 'Arn'], + }, + assumeRoleArn: { + 'Fn::Join': [ + '', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::222222222222:role/AWSAccelerator-CrossAccount-SsmParameter-Role', + ], + ], + }, + invokingAccountID: '333333333333', + parameterAccountID: '222222222222', + parameterName: '/accelerator/kms/key-arn', + region: 'us-east-1', + }, + }, + }, + }); + }); + /** + * Lambda function IAM role resource configuration test + */ + test(`${testNamePrefix} Lambda function IAM role CustomSsmGetParameterValue resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomSsmGetParameterValueCustomResourceProviderRoleB3AFDDB2: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: ['ssm:GetParameters', 'ssm:GetParameter', 'ssm:DescribeParameters'], + Effect: 'Allow', + Resource: ['*'], + }, + { + Action: ['sts:AssumeRole'], + Effect: 'Allow', + Resource: [ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::222222222222:role/AWSAccelerator-CrossAccount-SsmParameter-Role', + ], + ], + }, + ], + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * Lambda function resource configuration test + */ + test(`${testNamePrefix} Lambda function resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomSsmGetParameterValueCustomResourceProviderHandlerAAD0E7EE: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomSsmGetParameterValueCustomResourceProviderRoleB3AFDDB2'], + Properties: { + Code: { + S3Bucket: 'cdk-hnb659fds-assets-333333333333-us-east-1', + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomSsmGetParameterValueCustomResourceProviderRoleB3AFDDB2', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * CloudWatch log group resource configuration test + */ + test(`${testNamePrefix} CloudWatch log group resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomSsmGetParameterValueCustomResourceProviderLogGroup780D220D: { + Type: 'AWS::Logs::LogGroup', + Properties: { + LogGroupName: { + 'Fn::Join': [ + '', + ['/aws/lambda/', { Ref: 'CustomSsmGetParameterValueCustomResourceProviderHandlerAAD0E7EE' }], + ], + }, + RetentionInDays: 3653, + }, + }, + }, + }); + }); + + /** + * SSM parameter SsmParamStackId resource configuration test + */ + test(`${testNamePrefix} SSM parameter SsmParamStackId resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + SsmParamStackId521A78D3: { + Type: 'AWS::SSM::Parameter', + Properties: { + Name: '/accelerator/AWSAccelerator-NetworkVpcEndpointsStack-333333333333-us-east-1/stack-id', + Type: 'String', + Value: { + Ref: 'AWS::StackId', + }, + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/accelerator/test/network-vpc-stack.test.ts b/source/packages/@aws-accelerator/accelerator/test/network-vpc-stack.test.ts new file mode 100644 index 000000000..e96a7ed56 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/test/network-vpc-stack.test.ts @@ -0,0 +1,228 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import * as path from 'path'; + +import { AcceleratorStackNames } from '../lib/accelerator'; +import { AcceleratorStage } from '../lib/accelerator-stage'; +import { AcceleratorStackProps } from '../lib/stacks/accelerator-stack'; +import { NetworkVpcStack } from '../lib/stacks/network-vpc-stack'; +import { + ACCOUNT_CONFIG, + GLOBAL_CONFIG, + IAM_CONFIG, + NETWORK_CONFIG, + ORGANIZATION_CONFIG, + SECURITY_CONFIG, +} from './configs/test-config'; + +const testNamePrefix = 'Construct(NetworkVpcStack): '; + +/** + * NetworkVpcStack + */ +const app = new cdk.App({ + context: { 'config-dir': path.join(__dirname, 'configs') }, +}); +const configDirPath = app.node.tryGetContext('config-dir'); + +const env = { + account: '333333333333', + region: 'us-east-1', +}; + +const props: AcceleratorStackProps = { + env, + configDirPath, + accountsConfig: ACCOUNT_CONFIG, + globalConfig: GLOBAL_CONFIG, + iamConfig: IAM_CONFIG, + networkConfig: NETWORK_CONFIG, + organizationConfig: ORGANIZATION_CONFIG, + securityConfig: SECURITY_CONFIG, + partition: 'aws', +}; + +const stack = new NetworkVpcStack( + app, + `${AcceleratorStackNames[AcceleratorStage.NETWORK_VPC]}-${env.account}-${env.region}`, + props, +); + +/** + * NetworkVpcStack construct test + */ +describe('NetworkVpcStack', () => { + /** + * Number of Lambda function resource test + */ + test(`${testNamePrefix} Lambda function resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 2); + }); + + /** + * Number of Lambda function IAM role resource test + */ + test(`${testNamePrefix} Lambda function IAM role resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 2); + }); + + /** + * Number of DeleteDefaultVpc custom resource test + */ + test(`${testNamePrefix} DeleteDefaultVpc custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::DeleteDefaultVpc', 1); + }); + + /** + * Number of SSM parameter resource test + */ + test(`${testNamePrefix} SSM parameter custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::SSM::Parameter', 3); + }); + + /** + * Number of Prefix Lists resource test + */ + test(`${testNamePrefix} Prefix List custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::EC2::PrefixList', 1); + }); + + /** + * Lambda function CustomDeleteDefaultVpcCustomResourceProviderHandler resource configuration test + */ + test(`${testNamePrefix} Lambda function CustomDeleteDefaultVpcCustomResourceProviderHandler resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomDeleteDefaultVpcCustomResourceProviderHandler87E89F35: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomDeleteDefaultVpcCustomResourceProviderRole80963EEF'], + Properties: { + Code: { + S3Bucket: 'cdk-hnb659fds-assets-333333333333-us-east-1', + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomDeleteDefaultVpcCustomResourceProviderRole80963EEF', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * Lambda function IAM role CustomDeleteDefaultVpcCustomResourceProviderRole resource configuration test + */ + test(`${testNamePrefix} Lambda function IAM role CustomDeleteDefaultVpcCustomResourceProviderRole resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomDeleteDefaultVpcCustomResourceProviderRole80963EEF: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: [ + 'ec2:DeleteInternetGateway', + 'ec2:DetachInternetGateway', + 'ec2:DeleteNetworkAcl', + 'ec2:DeleteRoute', + 'ec2:DeleteSecurityGroup', + 'ec2:DeleteSubnet', + 'ec2:DeleteVpc', + 'ec2:DescribeInternetGateways', + 'ec2:DescribeNetworkAcls', + 'ec2:DescribeRouteTables', + 'ec2:DescribeSecurityGroups', + 'ec2:DescribeSubnets', + 'ec2:DescribeVpcs', + ], + Effect: 'Allow', + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * DeleteDefaultVpc custom resource configuration test + */ + test(`${testNamePrefix} DeleteDefaultVpc custom resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + DeleteDefaultVpc4DBAE36C: { + Type: 'Custom::DeleteDefaultVpc', + UpdateReplacePolicy: 'Delete', + DeletionPolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomDeleteDefaultVpcCustomResourceProviderHandler87E89F35', 'Arn'], + }, + }, + }, + }, + }); + }); + + /** + * SSM parameter SsmParamStackId resource configuration test + */ + test(`${testNamePrefix} SSM parameter SsmParamStackId resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + SsmParamStackId521A78D3: { + Type: 'AWS::SSM::Parameter', + Properties: { + Name: '/accelerator/AWSAccelerator-NetworkVpcStack-333333333333-us-east-1/stack-id', + Type: 'String', + Value: { + Ref: 'AWS::StackId', + }, + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/accelerator/test/operations-stack.test.ts b/source/packages/@aws-accelerator/accelerator/test/operations-stack.test.ts new file mode 100644 index 000000000..01ac23825 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/test/operations-stack.test.ts @@ -0,0 +1,436 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { SynthUtils } from '@aws-cdk/assert'; +import { OperationsStack } from '../lib/stacks/operations-stack'; +import { AcceleratorStackNames } from '../lib/accelerator'; +import { AcceleratorStage } from '../lib/accelerator-stage'; +import { + ACCOUNT_CONFIG, + GLOBAL_CONFIG, + IAM_CONFIG, + NETWORK_CONFIG, + ORGANIZATION_CONFIG, + SECURITY_CONFIG, +} from './configs/test-config'; +import * as path from 'path'; +import { AcceleratorStackProps } from '../lib/stacks/accelerator-stack'; + +const testNamePrefix = 'Construct(OperationsStack): '; + +/** + * OperationsStack + */ +const app = new cdk.App({ + context: { 'config-dir': path.join(__dirname, 'configs') }, +}); +const configDirPath = app.node.tryGetContext('config-dir'); + +const env = { + account: '333333333333', + region: 'us-east-1', +}; + +const props: AcceleratorStackProps = { + env, + configDirPath, + accountsConfig: ACCOUNT_CONFIG, + globalConfig: GLOBAL_CONFIG, + iamConfig: IAM_CONFIG, + networkConfig: NETWORK_CONFIG, + organizationConfig: ORGANIZATION_CONFIG, + securityConfig: SECURITY_CONFIG, + partition: 'aws', +}; + +const stack = new OperationsStack( + app, + `${AcceleratorStackNames[AcceleratorStage.OPERATIONS]}-${env.account}-${env.region}`, + props, +); + +/** + * OperationsStack construct test + */ +describe('OperationsStack', () => { + /** + * Snapshot test + */ + test(`${testNamePrefix} Snapshot Test`, () => { + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); + }); + + /** + * Number of IAM group resource test + */ + test(`${testNamePrefix} IAM group resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Group', 1); + }); + + /** + * Number of IAM user resource test + */ + test(`${testNamePrefix} IAM user resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::User', 2); + }); + + /** + * Number of SecretsManager secret resource test + */ + test(`${testNamePrefix} SecretsManager secret resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::SecretsManager::Secret', 2); + }); + + /** + * Number of IAM managedPolicy resource test + */ + test(`${testNamePrefix} IAM managedPolicy resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::ManagedPolicy', 1); + }); + + /** + * Number of IAM role resource test + */ + test(`${testNamePrefix} IAM role resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 1); + }); + + /** + * Number of IAM InstanceProfile resource test + */ + test(`${testNamePrefix} IAM InstanceProfile resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::InstanceProfile', 1); + }); + + /** + * Number of IAM SAMLProvider resource test + */ + test(`${testNamePrefix} IAM SAMLProvider resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::SAMLProvider', 1); + }); + + /** + * Number of SSM parameter resource test + */ + test(`${testNamePrefix} SSM parameter resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::SSM::Parameter', 2); + }); + + /** + * IAM group Administrators resource configuration test + */ + test(`${testNamePrefix} IAM group Administrators resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + AdministratorsA37EF73A: { + Type: 'AWS::IAM::Group', + Properties: { + GroupName: 'Administrators', + ManagedPolicyArns: [ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::aws:policy/AdministratorAccess', + ], + ], + }, + ], + }, + }, + }, + }); + }); + + /** + * IAM user BreakGlassUser01 resource configuration test + */ + test(`${testNamePrefix} IAM user BreakGlassUser01 resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + BreakGlassUser01AA051328: { + Type: 'AWS::IAM::User', + Properties: { + Groups: [ + { + Ref: 'AdministratorsA37EF73A', + }, + ], + LoginProfile: { + Password: { + 'Fn::Join': [ + '', + [ + '{{resolve:secretsmanager:', + { + Ref: 'BreakGlassUser01Secret8A54324D', + }, + ':SecretString:::}}', + ], + ], + }, + }, + PermissionsBoundary: { + Ref: 'DefaultBoundaryPolicy489A8D26', + }, + UserName: 'breakGlassUser01', + }, + }, + }, + }); + }); + + /** + * SecretsManager secret BreakGlassUser01Secret resource configuration test + */ + test(`${testNamePrefix} SecretsManager secret BreakGlassUser01Secret resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + BreakGlassUser01Secret8A54324D: { + Type: 'AWS::SecretsManager::Secret', + UpdateReplacePolicy: 'Delete', + DeletionPolicy: 'Delete', + Properties: { + GenerateSecretString: { + GenerateStringKey: 'password', + SecretStringTemplate: '{"username":"breakGlassUser01"}', + }, + Name: '/accelerator/breakGlassUser01', + }, + }, + }, + }); + }); + + /** + * IAM user BreakGlassUser02 resource configuration test + */ + test(`${testNamePrefix} IAM user BreakGlassUser02 resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + BreakGlassUser02DFF444C8: { + Type: 'AWS::IAM::User', + Properties: { + Groups: [ + { + Ref: 'AdministratorsA37EF73A', + }, + ], + LoginProfile: { + Password: { + 'Fn::Join': [ + '', + [ + '{{resolve:secretsmanager:', + { + Ref: 'BreakGlassUser02Secret4D200D8D', + }, + ':SecretString:::}}', + ], + ], + }, + }, + PermissionsBoundary: { + Ref: 'DefaultBoundaryPolicy489A8D26', + }, + UserName: 'breakGlassUser02', + }, + }, + }, + }); + }); + + /** + * SecretsManager secret BreakGlassUser02Secret resource configuration test + */ + test(`${testNamePrefix} SecretsManager secret BreakGlassUser02Secret resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + BreakGlassUser02Secret4D200D8D: { + Type: 'AWS::SecretsManager::Secret', + UpdateReplacePolicy: 'Delete', + DeletionPolicy: 'Delete', + Properties: { + GenerateSecretString: { + GenerateStringKey: 'password', + SecretStringTemplate: '{"username":"breakGlassUser02"}', + }, + Name: '/accelerator/breakGlassUser02', + }, + }, + }, + }); + }); + + /** + * IAM managedPolicy DefaultBoundaryPolicy resource configuration test + */ + test(`${testNamePrefix} IAM managedPolicy DefaultBoundaryPolicy resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + DefaultBoundaryPolicy489A8D26: { + Type: 'AWS::IAM::ManagedPolicy', + Properties: { + Description: '', + ManagedPolicyName: 'Default-Boundary-Policy', + Path: '/', + PolicyDocument: { + Statement: [ + { + Action: '*', + Effect: 'Allow', + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + }, + }, + }, + }); + }); + + /** + * IAM role Ec2DefaultSsmAdRole resource configuration test + */ + test(`${testNamePrefix} IAM role Ec2DefaultSsmAdRole resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + Ec2DefaultSsmAdRoleADFFA4C6: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'ec2.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::aws:policy/AmazonSSMManagedInstanceCore', + ], + ], + }, + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::aws:policy/AmazonSSMDirectoryServiceAccess', + ], + ], + }, + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::aws:policy/CloudWatchAgentServerPolicy', + ], + ], + }, + ], + PermissionsBoundary: { + Ref: 'DefaultBoundaryPolicy489A8D26', + }, + RoleName: 'EC2-Default-SSM-AD-Role', + }, + }, + }, + }); + }); + + /** + * IAM InstanceProfile Ec2DefaultSsmAdRoleInstanceProfile resource configuration test + */ + test(`${testNamePrefix} IAM InstanceProfile Ec2DefaultSsmAdRoleInstanceProfile resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + Ec2DefaultSsmAdRoleInstanceProfile: { + Type: 'AWS::IAM::InstanceProfile', + Properties: { + InstanceProfileName: { + Ref: 'Ec2DefaultSsmAdRoleADFFA4C6', + }, + Roles: [ + { + Ref: 'Ec2DefaultSsmAdRoleADFFA4C6', + }, + ], + }, + }, + }, + }); + }); + + /** + * IAM SAMLProvider ProviderSamlProvider resource configuration test + */ + test(`${testNamePrefix} IAM SAMLProvider ProviderSamlProvider resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + ProviderSamlProviderDA84AD16: { + Type: 'AWS::IAM::SAMLProvider', + Properties: { + Name: 'provider', + SamlMetadataDocument: '', + }, + }, + }, + }); + }); + + /** + * SSM parameter SsmParamStackId resource configuration test + */ + test(`${testNamePrefix} SSM parameter SsmParamStackId resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + SsmParamStackId521A78D3: { + Type: 'AWS::SSM::Parameter', + Properties: { + Name: '/accelerator/AWSAccelerator-OperationsStack-333333333333-us-east-1/stack-id', + Type: 'String', + Value: { + Ref: 'AWS::StackId', + }, + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/accelerator/test/organizations-stack.test.ts b/source/packages/@aws-accelerator/accelerator/test/organizations-stack.test.ts new file mode 100644 index 000000000..b49498830 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/test/organizations-stack.test.ts @@ -0,0 +1,880 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import * as path from 'path'; + +import { AcceleratorStackNames } from '../lib/accelerator'; +import { AcceleratorStage } from '../lib/accelerator-stage'; +import { AcceleratorStackProps } from '../lib/stacks/accelerator-stack'; +import { OrganizationsStack } from '../lib/stacks/organizations-stack'; +import { + ACCOUNT_CONFIG, + GLOBAL_CONFIG, + IAM_CONFIG, + NETWORK_CONFIG, + ORGANIZATION_CONFIG, + SECURITY_CONFIG, +} from './configs/test-config'; + +const testNamePrefix = 'Construct(OrganizationsStack): '; + +/** + * OrganizationsStack + */ +const app = new cdk.App({ + context: { 'config-dir': path.join(__dirname, 'configs') }, +}); +const configDirPath = app.node.tryGetContext('config-dir'); + +const env = { + account: '333333333333', + region: 'us-east-1', +}; + +const props: AcceleratorStackProps = { + env, + configDirPath, + accountsConfig: ACCOUNT_CONFIG, + globalConfig: GLOBAL_CONFIG, + iamConfig: IAM_CONFIG, + networkConfig: NETWORK_CONFIG, + organizationConfig: ORGANIZATION_CONFIG, + securityConfig: SECURITY_CONFIG, + partition: 'aws', +}; + +const stack = new OrganizationsStack( + app, + `${AcceleratorStackNames[AcceleratorStage.ORGANIZATIONS]}-${env.account}-${env.region}`, + props, +); + +/** + * OrganizationsStack construct test + */ +describe('OrganizationsStack', () => { + /** + * Number of IAM ServiceLinkedRole resource test + */ + test(`${testNamePrefix} IAM ServiceLinkedRole resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::ServiceLinkedRole', 1); + }); + + /** + * Number of Lambda function resource test + */ + test(`${testNamePrefix} Lambda function resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 10); + }); + + /** + * Number of Lambda IAM role resource test + */ + test(`${testNamePrefix} Lambda IAM role resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 11); + }); + + /** + * Number of EnableAwsServiceAccess custom resource test + */ + test(`${testNamePrefix} EnableAwsServiceAccess custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::EnableAwsServiceAccess', 1); + }); + + /** + * Number of EnableSharingWithAwsOrganization custom resource test + */ + test(`${testNamePrefix} EnableSharingWithAwsOrganization custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::EnableSharingWithAwsOrganization', 1); + }); + + /** + * Number of GuardDutyEnableOrganizationAdminAccount custom resource test + */ + test(`${testNamePrefix} GuardDutyEnableOrganizationAdminAccount custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::GuardDutyEnableOrganizationAdminAccount', 1); + }); + + /** + * Number of MacieEnableOrganizationAdminAccount custom resource test + */ + test(`${testNamePrefix} MacieEnableOrganizationAdminAccount custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::MacieEnableOrganizationAdminAccount', 1); + }); + + /** + * Number of OrganizationsRegisterDelegatedAdministrator custom resource test + */ + test(`${testNamePrefix} OrganizationsRegisterDelegatedAdministrator custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::OrganizationsRegisterDelegatedAdministrator', 1); + }); + + /** + * Number of SecurityHubEnableOrganizationAdminAccount custom resource test + */ + test(`${testNamePrefix} SecurityHubEnableOrganizationAdminAccount custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::SecurityHubEnableOrganizationAdminAccount', 1); + }); + + /** + * IAM ServiceLinkedRole AccessAnalyzerServiceLinkedRole resource configuration test + */ + test(`${testNamePrefix} IAM ServiceLinkedRole AccessAnalyzerServiceLinkedRole resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + AccessAnalyzerServiceLinkedRole: { + Type: 'AWS::IAM::ServiceLinkedRole', + Properties: { + AWSServiceName: 'access-analyzer.amazonaws.com', + }, + }, + }, + }); + }); + + /** + * Lambda function CustomEnableSharingWithAwsOrganizationCustomResourceProviderHandler resource configuration test + */ + test(`${testNamePrefix} Lambda function CustomEnableSharingWithAwsOrganizationCustomResourceProviderHandler resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomEnableSharingWithAwsOrganizationCustomResourceProviderHandler405D7398: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomEnableSharingWithAwsOrganizationCustomResourceProviderRole4FE5EBD7'], + Properties: { + Code: { + S3Bucket: 'cdk-hnb659fds-assets-333333333333-us-east-1', + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomEnableSharingWithAwsOrganizationCustomResourceProviderRole4FE5EBD7', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * Lambda IAM role CustomEnableSharingWithAwsOrganizationCustomResourceProviderRole resource configuration test + */ + test(`${testNamePrefix} Lambda IAM role CustomEnableSharingWithAwsOrganizationCustomResourceProviderRole resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomEnableSharingWithAwsOrganizationCustomResourceProviderRole4FE5EBD7: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: [ + 'ram:EnableSharingWithAwsOrganization', + 'iam:CreateServiceLinkedRole', + 'organizations:EnableAWSServiceAccess', + 'organizations:ListAWSServiceAccessForOrganization', + 'organizations:DescribeOrganization', + ], + Effect: 'Allow', + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * Lambda function CustomGuardDutyEnableOrganizationAdminAccountCustomResourceProviderHandler resource configuration test + */ + test(`${testNamePrefix} Lambda function CustomGuardDutyEnableOrganizationAdminAccountCustomResourceProviderHandler resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomGuardDutyEnableOrganizationAdminAccountCustomResourceProviderHandler1EC01026: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomGuardDutyEnableOrganizationAdminAccountCustomResourceProviderRole30371E09'], + Properties: { + Code: { + S3Bucket: 'cdk-hnb659fds-assets-333333333333-us-east-1', + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomGuardDutyEnableOrganizationAdminAccountCustomResourceProviderRole30371E09', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * Lambda IAM role CustomGuardDutyEnableOrganizationAdminAccountCustomResourceProviderRole resource configuration test + */ + test(`${testNamePrefix} Lambda IAM role CustomGuardDutyEnableOrganizationAdminAccountCustomResourceProviderRole resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomGuardDutyEnableOrganizationAdminAccountCustomResourceProviderRole30371E09: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: [ + 'organizations:DeregisterDelegatedAdministrator', + 'organizations:DescribeOrganization', + 'organizations:EnableAWSServiceAccess', + 'organizations:ListAWSServiceAccessForOrganization', + 'organizations:ListAccounts', + 'organizations:ListDelegatedAdministrators', + 'organizations:RegisterDelegatedAdministrator', + 'organizations:ServicePrincipal', + 'organizations:UpdateOrganizationConfiguration', + ], + Condition: { + StringLikeIfExists: { + 'organizations:DeregisterDelegatedAdministrator': ['guardduty.amazonaws.com'], + 'organizations:DescribeOrganization': ['guardduty.amazonaws.com'], + 'organizations:EnableAWSServiceAccess': ['guardduty.amazonaws.com'], + 'organizations:ListAWSServiceAccessForOrganization': ['guardduty.amazonaws.com'], + 'organizations:ListAccounts': ['guardduty.amazonaws.com'], + 'organizations:ListDelegatedAdministrators': ['guardduty.amazonaws.com'], + 'organizations:RegisterDelegatedAdministrator': ['guardduty.amazonaws.com'], + 'organizations:ServicePrincipal': ['guardduty.amazonaws.com'], + 'organizations:UpdateOrganizationConfiguration': ['guardduty.amazonaws.com'], + }, + }, + Effect: 'Allow', + Resource: '*', + Sid: 'GuardDutyEnableOrganizationAdminAccountTaskOrganizationActions', + }, + { + Action: [ + 'GuardDuty:EnableOrganizationAdminAccount', + 'GuardDuty:ListOrganizationAdminAccounts', + 'guardduty:DisableOrganizationAdminAccount', + ], + Effect: 'Allow', + Resource: '*', + Sid: 'GuardDutyEnableOrganizationAdminAccountTaskGuardDutyActions', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * Lambda function CustomMacieEnableOrganizationAdminAccountCustomResourceProviderHandler resource configuration test + */ + test(`${testNamePrefix} Lambda function CustomMacieEnableOrganizationAdminAccountCustomResourceProviderHandler resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomMacieEnableOrganizationAdminAccountCustomResourceProviderHandlerD7A9976A: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomMacieEnableOrganizationAdminAccountCustomResourceProviderRoleA386B194'], + Properties: { + Code: { + S3Bucket: 'cdk-hnb659fds-assets-333333333333-us-east-1', + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomMacieEnableOrganizationAdminAccountCustomResourceProviderRoleA386B194', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * Lambda IAM role CustomMacieEnableOrganizationAdminAccountCustomResourceProviderRole resource configuration test + */ + test(`${testNamePrefix} Lambda IAM role CustomMacieEnableOrganizationAdminAccountCustomResourceProviderRole resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomMacieEnableOrganizationAdminAccountCustomResourceProviderRoleA386B194: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: [ + 'organizations:DeregisterDelegatedAdministrator', + 'organizations:DescribeOrganization', + 'organizations:EnableAWSServiceAccess', + 'organizations:ListAWSServiceAccessForOrganization', + 'organizations:ListAccounts', + 'organizations:ListDelegatedAdministrators', + 'organizations:RegisterDelegatedAdministrator', + 'organizations:ServicePrincipal', + 'organizations:UpdateOrganizationConfiguration', + ], + Condition: { + StringLikeIfExists: { + 'organizations:DeregisterDelegatedAdministrator': ['macie.amazonaws.com'], + 'organizations:DescribeOrganization': ['macie.amazonaws.com'], + 'organizations:EnableAWSServiceAccess': ['macie.amazonaws.com'], + 'organizations:ListAWSServiceAccessForOrganization': ['macie.amazonaws.com'], + 'organizations:ListAccounts': ['macie.amazonaws.com'], + 'organizations:ListDelegatedAdministrators': ['macie.amazonaws.com'], + 'organizations:RegisterDelegatedAdministrator': ['macie.amazonaws.com'], + 'organizations:ServicePrincipal': ['macie.amazonaws.com'], + 'organizations:UpdateOrganizationConfiguration': ['macie.amazonaws.com'], + }, + }, + Effect: 'Allow', + Resource: '*', + Sid: 'MacieEnableOrganizationAdminAccountTaskOrganizationActions', + }, + { + Action: [ + 'macie2:DisableOrganizationAdminAccount', + 'macie2:EnableMacie', + 'macie2:EnableOrganizationAdminAccount', + 'macie2:GetMacieSession', + 'macie2:ListOrganizationAdminAccounts', + 'macie2:DisableOrganizationAdminAccount', + 'macie2:GetMacieSession', + 'macie2:EnableMacie', + ], + Effect: 'Allow', + Resource: '*', + Sid: 'MacieEnableOrganizationAdminAccountTaskMacieActions', + }, + { + Action: ['iam:CreateServiceLinkedRole'], + Condition: { + StringLikeIfExists: { + 'iam:CreateServiceLinkedRole': ['macie.amazonaws.com'], + }, + }, + Effect: 'Allow', + Resource: '*', + Sid: 'MacieEnableMacieTaskIamAction', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * Lambda function CustomOrganizationsEnableAwsServiceAccessCustomResourceProviderHandler resource configuration test + */ + test(`${testNamePrefix} Lambda function CustomOrganizationsEnableAwsServiceAccessCustomResourceProviderHandler resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomOrganizationsEnableAwsServiceAccessCustomResourceProviderHandlerDCD56D71: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomOrganizationsEnableAwsServiceAccessCustomResourceProviderRole59F76BA2'], + Properties: { + Code: { + S3Bucket: 'cdk-hnb659fds-assets-333333333333-us-east-1', + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomOrganizationsEnableAwsServiceAccessCustomResourceProviderRole59F76BA2', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * Lambda IAM role CustomOrganizationsEnableAwsServiceAccessCustomResourceProviderRole resource configuration test + */ + test(`${testNamePrefix} Lambda IAM role CustomOrganizationsEnableAwsServiceAccessCustomResourceProviderRole resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomOrganizationsEnableAwsServiceAccessCustomResourceProviderRole59F76BA2: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: ['organizations:DisableAWSServiceAccess', 'organizations:EnableAwsServiceAccess'], + Effect: 'Allow', + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * Lambda function CustomOrganizationsRegisterDelegatedAdministratorCustomResourceProviderHandler resource configuration test + */ + test(`${testNamePrefix} Lambda function CustomOrganizationsRegisterDelegatedAdministratorCustomResourceProviderHandler resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomOrganizationsRegisterDelegatedAdministratorCustomResourceProviderHandlerFAEA655C: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomOrganizationsRegisterDelegatedAdministratorCustomResourceProviderRole4B3EAD1B'], + Properties: { + Code: { + S3Bucket: 'cdk-hnb659fds-assets-333333333333-us-east-1', + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': [ + 'CustomOrganizationsRegisterDelegatedAdministratorCustomResourceProviderRole4B3EAD1B', + 'Arn', + ], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * Lambda IAM role CustomOrganizationsRegisterDelegatedAdministratorCustomResourceProviderRole resource configuration test + */ + test(`${testNamePrefix} Lambda IAM role CustomOrganizationsRegisterDelegatedAdministratorCustomResourceProviderRole resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomOrganizationsRegisterDelegatedAdministratorCustomResourceProviderRole4B3EAD1B: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: [ + 'organizations:DeregisterDelegatedAdministrator', + 'organizations:RegisterDelegatedAdministrator', + ], + Effect: 'Allow', + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * Lambda function CustomSecurityHubEnableOrganizationAdminAccountCustomResourceProviderHandler resource configuration test + */ + test(`${testNamePrefix} Lambda function CustomSecurityHubEnableOrganizationAdminAccountCustomResourceProviderHandler resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomSecurityHubEnableOrganizationAdminAccountCustomResourceProviderHandler194C30B9: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomSecurityHubEnableOrganizationAdminAccountCustomResourceProviderRole1CBC866F'], + Properties: { + Code: { + S3Bucket: 'cdk-hnb659fds-assets-333333333333-us-east-1', + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': [ + 'CustomSecurityHubEnableOrganizationAdminAccountCustomResourceProviderRole1CBC866F', + 'Arn', + ], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * Lambda IAM role CustomSecurityHubEnableOrganizationAdminAccountCustomResourceProviderRole resource configuration test + */ + test(`${testNamePrefix} Lambda IAM role CustomSecurityHubEnableOrganizationAdminAccountCustomResourceProviderRole resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomSecurityHubEnableOrganizationAdminAccountCustomResourceProviderRole1CBC866F: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: [ + 'organizations:DescribeOrganization', + 'organizations:ListAccounts', + 'organizations:ListDelegatedAdministrators', + ], + Effect: 'Allow', + Resource: '*', + Sid: 'SecurityHubEnableOrganizationAdminAccountTaskOrganizationActions', + }, + { + Action: 'organizations:EnableAWSServiceAccess', + Condition: { + StringEquals: { + 'organizations:ServicePrincipal': 'securityhub.amazonaws.com', + }, + }, + Effect: 'Allow', + Resource: '*', + }, + { + Action: [ + 'organizations:RegisterDelegatedAdministrator', + 'organizations:DeregisterDelegatedAdministrator', + ], + Condition: { + StringEquals: { + 'organizations:ServicePrincipal': 'securityhub.amazonaws.com', + }, + }, + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':organizations::*:account/o-*/*', + ], + ], + }, + }, + { + Action: ['iam:CreateServiceLinkedRole'], + Condition: { + StringLike: { + 'iam:AWSServiceName': ['securityhub.amazonaws.com'], + }, + }, + Effect: 'Allow', + Resource: '*', + Sid: 'SecurityHubCreateMembersTaskIamAction', + }, + { + Action: [ + 'securityhub:DisableOrganizationAdminAccount', + 'securityhub:EnableOrganizationAdminAccount', + 'securityhub:EnableSecurityHub', + 'securityhub:ListOrganizationAdminAccounts', + ], + Effect: 'Allow', + Resource: '*', + Sid: 'SecurityHubEnableOrganizationAdminAccountTaskSecurityHubActions', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * EnableAwsServiceAccess custom resource configuration test + */ + test(`${testNamePrefix} EnableAwsServiceAccess custom resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + EnableAccessAnalyzerAFBAAEC3: { + Type: 'Custom::EnableAwsServiceAccess', + UpdateReplacePolicy: 'Delete', + DeletionPolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomOrganizationsEnableAwsServiceAccessCustomResourceProviderHandlerDCD56D71', 'Arn'], + }, + servicePrincipal: 'access-analyzer.amazonaws.com', + }, + }, + }, + }); + }); + + /** + * EnableSharingWithAwsOrganization custom resource configuration test + */ + test(`${testNamePrefix} EnableSharingWithAwsOrganization custom resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + EnableSharingWithAwsOrganization81D5714F: { + Type: 'Custom::EnableSharingWithAwsOrganization', + UpdateReplacePolicy: 'Delete', + DeletionPolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomEnableSharingWithAwsOrganizationCustomResourceProviderHandler405D7398', 'Arn'], + }, + }, + }, + }, + }); + }); + + /** + * GuardDutyEnableOrganizationAdminAccount custom resource configuration test + */ + test(`${testNamePrefix} GuardDutyEnableOrganizationAdminAccount custom resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + GuardDutyEnableOrganizationAdminAccount90D7393E: { + Type: 'Custom::GuardDutyEnableOrganizationAdminAccount', + UpdateReplacePolicy: 'Delete', + DeletionPolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': [ + 'CustomGuardDutyEnableOrganizationAdminAccountCustomResourceProviderHandler1EC01026', + 'Arn', + ], + }, + adminAccountId: '222222222222', + region: 'us-east-1', + }, + }, + }, + }); + }); + + /** + * MacieEnableOrganizationAdminAccount custom resource configuration test + */ + test(`${testNamePrefix} MacieEnableOrganizationAdminAccount custom resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + MacieOrganizationAdminAccount2C23317B: { + Type: 'Custom::MacieEnableOrganizationAdminAccount', + UpdateReplacePolicy: 'Delete', + DeletionPolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomMacieEnableOrganizationAdminAccountCustomResourceProviderHandlerD7A9976A', 'Arn'], + }, + adminAccountId: '222222222222', + region: 'us-east-1', + }, + }, + }, + }); + }); + + /** + * OrganizationsRegisterDelegatedAdministrator custom resource configuration test + */ + test(`${testNamePrefix} OrganizationsRegisterDelegatedAdministrator custom resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + RegisterDelegatedAdministratorAccessAnalyzerE0CB7BBC: { + Type: 'Custom::OrganizationsRegisterDelegatedAdministrator', + UpdateReplacePolicy: 'Delete', + DeletionPolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': [ + 'CustomOrganizationsRegisterDelegatedAdministratorCustomResourceProviderHandlerFAEA655C', + 'Arn', + ], + }, + accountId: '222222222222', + servicePrincipal: 'access-analyzer.amazonaws.com', + }, + }, + }, + }); + }); + /** + * SecurityHubEnableOrganizationAdminAccount custom resource configuration test + */ + test(`${testNamePrefix} SecurityHubEnableOrganizationAdminAccount custom resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + SecurityHubOrganizationAdminAccount71D5E029: { + Type: 'Custom::SecurityHubEnableOrganizationAdminAccount', + UpdateReplacePolicy: 'Delete', + DeletionPolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': [ + 'CustomSecurityHubEnableOrganizationAdminAccountCustomResourceProviderHandler194C30B9', + 'Arn', + ], + }, + adminAccountId: '222222222222', + region: 'us-east-1', + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/accelerator/test/pipeline-stack.test.ts b/source/packages/@aws-accelerator/accelerator/test/pipeline-stack.test.ts new file mode 100644 index 000000000..7fd507578 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/test/pipeline-stack.test.ts @@ -0,0 +1,1614 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +//import { SynthUtils } from '@aws-cdk/assert'; + +import { PipelineStack } from '../lib/stacks/pipeline-stack'; + +const testNamePrefix = 'Construct(PipelineStack): '; + +/** + * Pipeline Stack + */ +const app = new cdk.App(); +const stack = new PipelineStack(app, 'PipelineStack', { + sourceRepository: 'codecommit', + sourceRepositoryOwner: 'awslabs', + sourceRepositoryName: 'accelerator-source', + sourceBranchName: 'main', + enableApprovalStage: true, + qualifier: 'aws-accelerator', + managementAccountId: app.account, + managementAccountRoleName: 'AcceleratorAccountAccessRole', + managementAccountEmail: 'accelerator-root@mydomain.com', + logArchiveAccountEmail: 'accelerator-log-archive@mydomain.com', + auditAccountEmail: 'accelerator-audit@mydomain.com', + partition: 'aws', +}); + +/** + * PipelineStack construct test + */ +describe('PipelineStack', () => { + /** + * Snapshot test + */ + // test(`${testNamePrefix} Snapshot Test`, () => { + // expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); + // }); + /** + * Number of CodePipeline resource test + */ + test(`${testNamePrefix} CodePipeline resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::CodePipeline::Pipeline', 1); + }); + + /** + * Number of CodeBuild project resource test + */ + test(`${testNamePrefix} CodeBuild project resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::CodeBuild::Project', 2); + }); + + /** + * Number of IAM Role resource test + */ + test(`${testNamePrefix} IAM Role resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 6); + }); + + /** + * Number of IAM Policy resource test + */ + test(`${testNamePrefix} IAM Policy resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Policy', 6); + }); + + /** + * Number of CodeCommit Repository resource test + */ + test(`${testNamePrefix} CodeCommit Repository resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::CodeCommit::Repository', 1); + }); + + /** + * Number of S3 Bucket resource test + */ + test(`${testNamePrefix} S3 Bucket resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::S3::Bucket', 1); + }); + + /** + * Number of BucketPolicy resource test + */ + test(`${testNamePrefix} BucketPolicy resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::S3::BucketPolicy', 1); + }); + + /** + * Number of BucketPolicy resource test + */ + test(`${testNamePrefix} BucketPolicy resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::S3::BucketPolicy', 1); + }); + + /** + * CodePipeline resource configuration test + */ + test(`${testNamePrefix} CodePipeline resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + Pipeline8E4BFAC9: { + Type: 'AWS::CodePipeline::Pipeline', + DependsOn: [ + 'PipelineAWSServiceRoleForCodeStarNotificationsDA052A10', + 'PipelinePipelineRoleDefaultPolicy7D262A22', + 'PipelinePipelineRole6D983AD5', + ], + Properties: { + ArtifactStore: { + EncryptionKey: { + Id: { + Ref: 'SsmParameterValueacceleratorawsacceleratorinstallerkmskeyarnC96584B6F00A464EAD1953AFF4B05118Parameter', + }, + Type: 'KMS', + }, + Location: { + Ref: 'PipelineSecureBucketB3EEB324', + }, + Type: 'S3', + }, + Name: 'aws-accelerator-pipeline', + RoleArn: { + 'Fn::GetAtt': ['PipelinePipelineRole6D983AD5', 'Arn'], + }, + Stages: [ + { + Actions: [ + { + ActionTypeId: { + Category: 'Source', + Owner: 'AWS', + Provider: 'CodeCommit', + Version: '1', + }, + Configuration: { + BranchName: 'main', + PollForSourceChanges: false, + RepositoryName: 'accelerator-source', + }, + Name: 'Source', + OutputArtifacts: [ + { + Name: 'Source', + }, + ], + RoleArn: { + 'Fn::GetAtt': ['PipelineSourceCodePipelineActionRoleBBC58FD5', 'Arn'], + }, + RunOrder: 1, + }, + { + ActionTypeId: { + Category: 'Source', + Owner: 'AWS', + Provider: 'CodeCommit', + Version: '1', + }, + Configuration: { + BranchName: 'main', + PollForSourceChanges: false, + RepositoryName: { + 'Fn::GetAtt': ['PipelineConfigRepositoryE5225086', 'Name'], + }, + }, + Name: 'Configuration', + OutputArtifacts: [ + { + Name: 'Config', + }, + ], + RoleArn: { + 'Fn::GetAtt': ['PipelineSourceConfigurationCodePipelineActionRoleA2807B19', 'Arn'], + }, + RunOrder: 1, + }, + ], + Name: 'Source', + }, + { + Actions: [ + { + ActionTypeId: { + Category: 'Build', + Owner: 'AWS', + Provider: 'CodeBuild', + Version: '1', + }, + Configuration: { + PrimarySource: 'Source', + ProjectName: { + Ref: 'PipelineBuildProject9D447FA8', + }, + }, + InputArtifacts: [ + { + Name: 'Source', + }, + { + Name: 'Config', + }, + ], + Name: 'Build', + OutputArtifacts: [ + { + Name: 'Build', + }, + ], + RoleArn: { + 'Fn::GetAtt': ['PipelinePipelineRole6D983AD5', 'Arn'], + }, + RunOrder: 1, + }, + ], + Name: 'Build', + }, + { + Actions: [ + { + ActionTypeId: { + Category: 'Build', + Owner: 'AWS', + Provider: 'CodeBuild', + Version: '1', + }, + Configuration: { + EnvironmentVariables: + '[{"name":"CDK_OPTIONS","type":"PLAINTEXT","value":"deploy --stage prepare"},{"name":"CONFIG_COMMIT_ID","type":"PLAINTEXT","value":"#{Config-Vars.CommitId}"},{"name":"ACCELERATOR_STAGE","type":"PLAINTEXT","value":"prepare"}]', + PrimarySource: 'Build', + ProjectName: { + Ref: 'PipelineToolkitProjectBCBD6910', + }, + }, + InputArtifacts: [ + { + Name: 'Build', + }, + { + Name: 'Config', + }, + ], + Name: 'Prepare', + RoleArn: { + 'Fn::GetAtt': ['PipelinePipelineRole6D983AD5', 'Arn'], + }, + RunOrder: 1, + }, + ], + Name: 'Prepare', + }, + { + Actions: [ + { + ActionTypeId: { + Category: 'Build', + Owner: 'AWS', + Provider: 'CodeBuild', + Version: '1', + }, + Configuration: { + EnvironmentVariables: + '[{"name":"CDK_OPTIONS","type":"PLAINTEXT","value":"deploy --stage accounts"},{"name":"CONFIG_COMMIT_ID","type":"PLAINTEXT","value":"#{Config-Vars.CommitId}"},{"name":"ACCELERATOR_STAGE","type":"PLAINTEXT","value":"accounts"}]', + PrimarySource: 'Build', + ProjectName: { + Ref: 'PipelineToolkitProjectBCBD6910', + }, + }, + InputArtifacts: [ + { + Name: 'Build', + }, + { + Name: 'Config', + }, + ], + Name: 'Accounts', + RoleArn: { + 'Fn::GetAtt': ['PipelinePipelineRole6D983AD5', 'Arn'], + }, + RunOrder: 1, + }, + ], + Name: 'Accounts', + }, + { + Actions: [ + { + ActionTypeId: { + Category: 'Build', + Owner: 'AWS', + Provider: 'CodeBuild', + Version: '1', + }, + Configuration: { + EnvironmentVariables: + '[{"name":"CDK_OPTIONS","type":"PLAINTEXT","value":"bootstrap"},{"name":"CONFIG_COMMIT_ID","type":"PLAINTEXT","value":"#{Config-Vars.CommitId}"}]', + PrimarySource: 'Build', + ProjectName: { + Ref: 'PipelineToolkitProjectBCBD6910', + }, + }, + InputArtifacts: [ + { + Name: 'Build', + }, + { + Name: 'Config', + }, + ], + Name: 'Bootstrap', + RoleArn: { + 'Fn::GetAtt': ['PipelinePipelineRole6D983AD5', 'Arn'], + }, + RunOrder: 1, + }, + ], + Name: 'Bootstrap', + }, + { + Actions: [ + { + ActionTypeId: { + Category: 'Build', + Owner: 'AWS', + Provider: 'CodeBuild', + Version: '1', + }, + Configuration: { + EnvironmentVariables: + '[{"name":"CDK_OPTIONS","type":"PLAINTEXT","value":"diff"},{"name":"CONFIG_COMMIT_ID","type":"PLAINTEXT","value":"#{Config-Vars.CommitId}"}]', + PrimarySource: 'Build', + ProjectName: { + Ref: 'PipelineToolkitProjectBCBD6910', + }, + }, + InputArtifacts: [ + { + Name: 'Build', + }, + { + Name: 'Config', + }, + ], + Name: 'Diff', + RoleArn: { + 'Fn::GetAtt': ['PipelinePipelineRole6D983AD5', 'Arn'], + }, + RunOrder: 1, + }, + { + ActionTypeId: { + Category: 'Approval', + Owner: 'AWS', + Provider: 'Manual', + Version: '1', + }, + Configuration: { + CustomData: 'See previous stage (Diff) for changes.', + }, + Name: 'Approve', + RoleArn: { + 'Fn::GetAtt': ['PipelineReviewApproveCodePipelineActionRole3122ED42', 'Arn'], + }, + RunOrder: 2, + }, + ], + Name: 'Review', + }, + { + Actions: [ + { + ActionTypeId: { + Category: 'Build', + Owner: 'AWS', + Provider: 'CodeBuild', + Version: '1', + }, + Configuration: { + EnvironmentVariables: + '[{"name":"CDK_OPTIONS","type":"PLAINTEXT","value":"deploy --stage key"},{"name":"CONFIG_COMMIT_ID","type":"PLAINTEXT","value":"#{Config-Vars.CommitId}"},{"name":"ACCELERATOR_STAGE","type":"PLAINTEXT","value":"key"}]', + PrimarySource: 'Build', + ProjectName: { + Ref: 'PipelineToolkitProjectBCBD6910', + }, + }, + InputArtifacts: [ + { + Name: 'Build', + }, + { + Name: 'Config', + }, + ], + Name: 'Key', + RoleArn: { + 'Fn::GetAtt': ['PipelinePipelineRole6D983AD5', 'Arn'], + }, + RunOrder: 1, + }, + { + ActionTypeId: { + Category: 'Build', + Owner: 'AWS', + Provider: 'CodeBuild', + Version: '1', + }, + Configuration: { + EnvironmentVariables: + '[{"name":"CDK_OPTIONS","type":"PLAINTEXT","value":"deploy --stage logging"},{"name":"CONFIG_COMMIT_ID","type":"PLAINTEXT","value":"#{Config-Vars.CommitId}"},{"name":"ACCELERATOR_STAGE","type":"PLAINTEXT","value":"logging"}]', + PrimarySource: 'Build', + ProjectName: { + Ref: 'PipelineToolkitProjectBCBD6910', + }, + }, + InputArtifacts: [ + { + Name: 'Build', + }, + { + Name: 'Config', + }, + ], + Name: 'Logging', + RoleArn: { + 'Fn::GetAtt': ['PipelinePipelineRole6D983AD5', 'Arn'], + }, + RunOrder: 2, + }, + ], + Name: 'Logging', + }, + { + Actions: [ + { + ActionTypeId: { + Category: 'Build', + Owner: 'AWS', + Provider: 'CodeBuild', + Version: '1', + }, + Configuration: { + EnvironmentVariables: + '[{"name":"CDK_OPTIONS","type":"PLAINTEXT","value":"deploy --stage organizations"},{"name":"CONFIG_COMMIT_ID","type":"PLAINTEXT","value":"#{Config-Vars.CommitId}"},{"name":"ACCELERATOR_STAGE","type":"PLAINTEXT","value":"organizations"}]', + PrimarySource: 'Build', + ProjectName: { + Ref: 'PipelineToolkitProjectBCBD6910', + }, + }, + InputArtifacts: [ + { + Name: 'Build', + }, + { + Name: 'Config', + }, + ], + Name: 'Organizations', + RoleArn: { + 'Fn::GetAtt': ['PipelinePipelineRole6D983AD5', 'Arn'], + }, + RunOrder: 1, + }, + ], + Name: 'Organization', + }, + { + Actions: [ + { + ActionTypeId: { + Category: 'Build', + Owner: 'AWS', + Provider: 'CodeBuild', + Version: '1', + }, + Configuration: { + EnvironmentVariables: + '[{"name":"CDK_OPTIONS","type":"PLAINTEXT","value":"deploy --stage security-audit"},{"name":"CONFIG_COMMIT_ID","type":"PLAINTEXT","value":"#{Config-Vars.CommitId}"},{"name":"ACCELERATOR_STAGE","type":"PLAINTEXT","value":"security-audit"}]', + PrimarySource: 'Build', + ProjectName: { + Ref: 'PipelineToolkitProjectBCBD6910', + }, + }, + InputArtifacts: [ + { + Name: 'Build', + }, + { + Name: 'Config', + }, + ], + Name: 'SecurityAudit', + RoleArn: { + 'Fn::GetAtt': ['PipelinePipelineRole6D983AD5', 'Arn'], + }, + RunOrder: 1, + }, + ], + Name: 'SecurityAudit', + }, + { + Actions: [ + { + ActionTypeId: { + Category: 'Build', + Owner: 'AWS', + Provider: 'CodeBuild', + Version: '1', + }, + Configuration: { + EnvironmentVariables: + '[{"name":"CDK_OPTIONS","type":"PLAINTEXT","value":"deploy --stage network-prep"},{"name":"CONFIG_COMMIT_ID","type":"PLAINTEXT","value":"#{Config-Vars.CommitId}"},{"name":"ACCELERATOR_STAGE","type":"PLAINTEXT","value":"network-prep"}]', + PrimarySource: 'Build', + ProjectName: { + Ref: 'PipelineToolkitProjectBCBD6910', + }, + }, + InputArtifacts: [ + { + Name: 'Build', + }, + { + Name: 'Config', + }, + ], + Name: 'Network_Prepare', + RoleArn: { + 'Fn::GetAtt': ['PipelinePipelineRole6D983AD5', 'Arn'], + }, + RunOrder: 1, + }, + { + ActionTypeId: { + Category: 'Build', + Owner: 'AWS', + Provider: 'CodeBuild', + Version: '1', + }, + Configuration: { + EnvironmentVariables: + '[{"name":"CDK_OPTIONS","type":"PLAINTEXT","value":"deploy --stage security"},{"name":"CONFIG_COMMIT_ID","type":"PLAINTEXT","value":"#{Config-Vars.CommitId}"},{"name":"ACCELERATOR_STAGE","type":"PLAINTEXT","value":"security"}]', + PrimarySource: 'Build', + ProjectName: { + Ref: 'PipelineToolkitProjectBCBD6910', + }, + }, + InputArtifacts: [ + { + Name: 'Build', + }, + { + Name: 'Config', + }, + ], + Name: 'Security', + RoleArn: { + 'Fn::GetAtt': ['PipelinePipelineRole6D983AD5', 'Arn'], + }, + RunOrder: 1, + }, + { + ActionTypeId: { + Category: 'Build', + Owner: 'AWS', + Provider: 'CodeBuild', + Version: '1', + }, + Configuration: { + EnvironmentVariables: + '[{"name":"CDK_OPTIONS","type":"PLAINTEXT","value":"deploy --stage operations"},{"name":"CONFIG_COMMIT_ID","type":"PLAINTEXT","value":"#{Config-Vars.CommitId}"},{"name":"ACCELERATOR_STAGE","type":"PLAINTEXT","value":"operations"}]', + PrimarySource: 'Build', + ProjectName: { + Ref: 'PipelineToolkitProjectBCBD6910', + }, + }, + InputArtifacts: [ + { + Name: 'Build', + }, + { + Name: 'Config', + }, + ], + Name: 'Operations', + RoleArn: { + 'Fn::GetAtt': ['PipelinePipelineRole6D983AD5', 'Arn'], + }, + RunOrder: 1, + }, + { + ActionTypeId: { + Category: 'Build', + Owner: 'AWS', + Provider: 'CodeBuild', + Version: '1', + }, + Configuration: { + EnvironmentVariables: + '[{"name":"CDK_OPTIONS","type":"PLAINTEXT","value":"deploy --stage network-vpc"},{"name":"CONFIG_COMMIT_ID","type":"PLAINTEXT","value":"#{Config-Vars.CommitId}"},{"name":"ACCELERATOR_STAGE","type":"PLAINTEXT","value":"network-vpc"}]', + PrimarySource: 'Build', + ProjectName: { + Ref: 'PipelineToolkitProjectBCBD6910', + }, + }, + InputArtifacts: [ + { + Name: 'Build', + }, + { + Name: 'Config', + }, + ], + Name: 'Network_VPCs', + RoleArn: { + 'Fn::GetAtt': ['PipelinePipelineRole6D983AD5', 'Arn'], + }, + RunOrder: 2, + }, + { + ActionTypeId: { + Category: 'Build', + Owner: 'AWS', + Provider: 'CodeBuild', + Version: '1', + }, + Configuration: { + EnvironmentVariables: + '[{"name":"CDK_OPTIONS","type":"PLAINTEXT","value":"deploy --stage security-resources"},{"name":"CONFIG_COMMIT_ID","type":"PLAINTEXT","value":"#{Config-Vars.CommitId}"},{"name":"ACCELERATOR_STAGE","type":"PLAINTEXT","value":"security-resources"}]', + PrimarySource: 'Build', + ProjectName: { + Ref: 'PipelineToolkitProjectBCBD6910', + }, + }, + InputArtifacts: [ + { + Name: 'Build', + }, + { + Name: 'Config', + }, + ], + Name: 'Security_Resources', + RoleArn: { + 'Fn::GetAtt': ['PipelinePipelineRole6D983AD5', 'Arn'], + }, + RunOrder: 2, + }, + { + ActionTypeId: { + Category: 'Build', + Owner: 'AWS', + Provider: 'CodeBuild', + Version: '1', + }, + Configuration: { + EnvironmentVariables: + '[{"name":"CDK_OPTIONS","type":"PLAINTEXT","value":"deploy --stage network-associations"},{"name":"CONFIG_COMMIT_ID","type":"PLAINTEXT","value":"#{Config-Vars.CommitId}"},{"name":"ACCELERATOR_STAGE","type":"PLAINTEXT","value":"network-associations"}]', + PrimarySource: 'Build', + ProjectName: { + Ref: 'PipelineToolkitProjectBCBD6910', + }, + }, + InputArtifacts: [ + { + Name: 'Build', + }, + { + Name: 'Config', + }, + ], + Name: 'Network_Associations', + RoleArn: { + 'Fn::GetAtt': ['PipelinePipelineRole6D983AD5', 'Arn'], + }, + RunOrder: 3, + }, + { + ActionTypeId: { + Category: 'Build', + Owner: 'AWS', + Provider: 'CodeBuild', + Version: '1', + }, + Configuration: { + EnvironmentVariables: + '[{"name":"CDK_OPTIONS","type":"PLAINTEXT","value":"deploy --stage finalize"},{"name":"CONFIG_COMMIT_ID","type":"PLAINTEXT","value":"#{Config-Vars.CommitId}"},{"name":"ACCELERATOR_STAGE","type":"PLAINTEXT","value":"finalize"}]', + PrimarySource: 'Build', + ProjectName: { + Ref: 'PipelineToolkitProjectBCBD6910', + }, + }, + InputArtifacts: [ + { + Name: 'Build', + }, + { + Name: 'Config', + }, + ], + Name: 'Finalize', + RoleArn: { + 'Fn::GetAtt': ['PipelinePipelineRole6D983AD5', 'Arn'], + }, + RunOrder: 4, + }, + ], + Name: 'Deploy', + }, + ], + }, + }, + }, + }); + }); + + /** + * Build stage CodeBuild project resource configuration test + */ + test(`${testNamePrefix} Build stage CodeBuild project resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + PipelineBuildProject9D447FA8: { + Type: 'AWS::CodeBuild::Project', + Properties: { + Artifacts: { + Type: 'CODEPIPELINE', + }, + Cache: { + Modes: ['LOCAL_SOURCE_CACHE'], + Type: 'LOCAL', + }, + EncryptionKey: { + Ref: 'SsmParameterValueacceleratorawsacceleratorinstallerkmskeyarnC96584B6F00A464EAD1953AFF4B05118Parameter', + }, + Environment: { + ComputeType: 'BUILD_GENERAL1_MEDIUM', + EnvironmentVariables: [ + { + Name: 'NODE_OPTIONS', + Type: 'PLAINTEXT', + Value: '--max_old_space_size=4096', + }, + ], + Image: 'aws/codebuild/standard:5.0', + ImagePullCredentialsType: 'CODEBUILD', + PrivilegedMode: true, + Type: 'LINUX_CONTAINER', + }, + Name: 'aws-accelerator-build-project', + ServiceRole: { + 'Fn::GetAtt': ['PipelineBuildRoleDC686070', 'Arn'], + }, + Source: { + BuildSpec: + '{\n "version": "0.2",\n "phases": {\n "install": {\n "runtime-versions": {\n "nodejs": 14\n }\n },\n "build": {\n "commands": [\n "env",\n "cd source",\n "yarn install",\n "yarn lerna link",\n "yarn build"\n ]\n }\n },\n "artifacts": {\n "files": [\n "**/*"\n ],\n "enable-symlinks": "yes"\n }\n}', + Type: 'CODEPIPELINE', + }, + }, + }, + }, + }); + }); + + /** + * CodePipeline build role resource configuration test + */ + test(`${testNamePrefix} CodePipeline build role resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + PipelineBuildRoleDC686070: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'codebuild.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + }, + }, + }, + }); + }); + + /** + * CodePipeline build role iam policy resource configuration test + */ + test(`${testNamePrefix} CodePipeline build role iam policy resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + PipelineBuildRoleDefaultPolicy3DAB973E: { + Type: 'AWS::IAM::Policy', + Properties: { + PolicyDocument: { + Statement: [ + { + Action: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'], + Effect: 'Allow', + Resource: [ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':logs:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':log-group:/aws/codebuild/', + { + Ref: 'PipelineBuildProject9D447FA8', + }, + ], + ], + }, + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':logs:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':log-group:/aws/codebuild/', + { + Ref: 'PipelineBuildProject9D447FA8', + }, + ':*', + ], + ], + }, + ], + }, + { + Action: [ + 'codebuild:CreateReportGroup', + 'codebuild:CreateReport', + 'codebuild:UpdateReport', + 'codebuild:BatchPutTestCases', + 'codebuild:BatchPutCodeCoverages', + ], + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':codebuild:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':report-group/', + { + Ref: 'PipelineBuildProject9D447FA8', + }, + '-*', + ], + ], + }, + }, + { + Action: ['kms:Decrypt', 'kms:Encrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*'], + Effect: 'Allow', + Resource: { + Ref: 'SsmParameterValueacceleratorawsacceleratorinstallerkmskeyarnC96584B6F00A464EAD1953AFF4B05118Parameter', + }, + }, + { + Action: [ + 's3:GetObject*', + 's3:GetBucket*', + 's3:List*', + 's3:DeleteObject*', + 's3:PutObject', + 's3:PutObjectLegalHold', + 's3:PutObjectRetention', + 's3:PutObjectTagging', + 's3:PutObjectVersionTagging', + 's3:Abort*', + ], + Effect: 'Allow', + Resource: [ + { + 'Fn::GetAtt': ['PipelineSecureBucketB3EEB324', 'Arn'], + }, + { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': ['PipelineSecureBucketB3EEB324', 'Arn'], + }, + '/*', + ], + ], + }, + ], + }, + { + Action: ['kms:Decrypt', 'kms:DescribeKey', 'kms:Encrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*'], + Effect: 'Allow', + Resource: { + Ref: 'SsmParameterValueacceleratorawsacceleratorinstallerkmskeyarnC96584B6F00A464EAD1953AFF4B05118Parameter', + }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'PipelineBuildRoleDefaultPolicy3DAB973E', + Roles: [ + { + Ref: 'PipelineBuildRoleDC686070', + }, + ], + }, + }, + }, + }); + }); + + /** + * PipelineConfigRepository resource configuration test + */ + test(`${testNamePrefix} PipelineConfigRepository resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + PipelineConfigRepositoryE5225086: { + Type: 'AWS::CodeCommit::Repository', + Properties: { + Code: { + BranchName: 'main', + S3: { + Bucket: { + 'Fn::Sub': 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', + }, + }, + }, + RepositoryName: 'aws-accelerator-config', + }, + }, + }, + }); + }); + + /** + * PipelinePipelineRole resource configuration test + */ + test(`${testNamePrefix} PipelinePipelineRole resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + PipelinePipelineRole6D983AD5: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'codepipeline.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + }, + }, + }, + }); + }); + + /** + * PipelinePipelineRoleDefaultPolicy resource configuration test + */ + test(`${testNamePrefix} PipelinePipelineRoleDefaultPolicy resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + PipelinePipelineRoleDefaultPolicy7D262A22: { + Type: 'AWS::IAM::Policy', + Metadata: { + cdk_nag: { + rules_to_suppress: [ + { + id: 'AwsSolutions-IAM5', + reason: 'PipelineRole DefaultPolicy is built by cdk.', + }, + ], + }, + }, + Properties: { + PolicyDocument: { + Statement: [ + { + Action: [ + 's3:GetObject*', + 's3:GetBucket*', + 's3:List*', + 's3:DeleteObject*', + 's3:PutObject', + 's3:PutObjectLegalHold', + 's3:PutObjectRetention', + 's3:PutObjectTagging', + 's3:PutObjectVersionTagging', + 's3:Abort*', + ], + Effect: 'Allow', + Resource: [ + { + 'Fn::GetAtt': ['PipelineSecureBucketB3EEB324', 'Arn'], + }, + { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': ['PipelineSecureBucketB3EEB324', 'Arn'], + }, + '/*', + ], + ], + }, + ], + }, + { + Action: ['kms:Decrypt', 'kms:DescribeKey', 'kms:Encrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*'], + Effect: 'Allow', + Resource: { + Ref: 'SsmParameterValueacceleratorawsacceleratorinstallerkmskeyarnC96584B6F00A464EAD1953AFF4B05118Parameter', + }, + }, + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': ['PipelineSourceCodePipelineActionRoleBBC58FD5', 'Arn'], + }, + }, + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': ['PipelineSourceConfigurationCodePipelineActionRoleA2807B19', 'Arn'], + }, + }, + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': ['PipelinePipelineRole6D983AD5', 'Arn'], + }, + }, + { + Action: ['codebuild:BatchGetBuilds', 'codebuild:StartBuild', 'codebuild:StopBuild'], + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': ['PipelineBuildProject9D447FA8', 'Arn'], + }, + }, + { + Action: ['codebuild:BatchGetBuilds', 'codebuild:StartBuild', 'codebuild:StopBuild'], + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': ['PipelineToolkitProjectBCBD6910', 'Arn'], + }, + }, + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': ['PipelineReviewApproveCodePipelineActionRole3122ED42', 'Arn'], + }, + }, + { + Action: 'sns:Publish', + Effect: 'Allow', + Resource: { + Ref: 'PipelineAcceleratorStatusTopic2BD5793F', + }, + }, + { + Action: 'sns:Publish', + Effect: 'Allow', + Resource: { + Ref: 'PipelineAcceleratorFailedStatusTopic614002B3', + }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'PipelinePipelineRoleDefaultPolicy7D262A22', + Roles: [ + { + Ref: 'PipelinePipelineRole6D983AD5', + }, + ], + }, + }, + }, + }); + }); + + /** + * PipelineSecureBucket resource configuration test + */ + test(`${testNamePrefix} PipelineSecureBucket resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + PipelineSecureBucketB3EEB324: { + Type: 'AWS::S3::Bucket', + DeletionPolicy: 'Retain', + UpdateReplacePolicy: 'Retain', + Properties: { + BucketEncryption: { + ServerSideEncryptionConfiguration: [ + { + ServerSideEncryptionByDefault: { + KMSMasterKeyID: { + Ref: 'SsmParameterValueacceleratorawsacceleratorinstallerkmskeyarnC96584B6F00A464EAD1953AFF4B05118Parameter', + }, + SSEAlgorithm: 'aws:kms', + }, + }, + ], + }, + BucketName: { + 'Fn::Join': [ + '', + [ + 'aws-accelerator-pipeline-', + { + Ref: 'AWS::AccountId', + }, + '-', + { + Ref: 'AWS::Region', + }, + ], + ], + }, + OwnershipControls: { + Rules: [ + { + ObjectOwnership: 'BucketOwnerPreferred', + }, + ], + }, + PublicAccessBlockConfiguration: { + BlockPublicAcls: true, + BlockPublicPolicy: true, + IgnorePublicAcls: true, + RestrictPublicBuckets: true, + }, + VersioningConfiguration: { + Status: 'Enabled', + }, + }, + }, + }, + }); + }); + + /** + * PipelineSecureBucketPolicy resource configuration test + */ + test(`${testNamePrefix} PipelineSecureBucketPolicy resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + PipelineSecureBucketPolicy1BD98DDB: { + Type: 'AWS::S3::BucketPolicy', + Properties: { + Bucket: { + Ref: 'PipelineSecureBucketB3EEB324', + }, + PolicyDocument: { + Statement: [ + { + Action: 's3:*', + Condition: { + Bool: { + 'aws:SecureTransport': 'false', + }, + }, + Effect: 'Deny', + Principal: { + AWS: '*', + }, + Resource: [ + { + 'Fn::GetAtt': ['PipelineSecureBucketB3EEB324', 'Arn'], + }, + { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': ['PipelineSecureBucketB3EEB324', 'Arn'], + }, + '/*', + ], + ], + }, + ], + Sid: 'deny-insecure-connections', + }, + ], + Version: '2012-10-17', + }, + }, + }, + }, + }); + }); + + /** + * PipelineSourceCodePipelineActionRole resource configuration test + */ + test(`${testNamePrefix} PipelineSourceCodePipelineActionRole resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + PipelineSourceCodePipelineActionRoleBBC58FD5: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + AWS: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::', + { + Ref: 'AWS::AccountId', + }, + ':root', + ], + ], + }, + }, + }, + ], + Version: '2012-10-17', + }, + }, + }, + }, + }); + }); + + /** + * PipelineSourceCodePipelineActionRoleDefaultPolicy resource configuration test + */ + test(`${testNamePrefix} PipelineSourceCodePipelineActionRoleDefaultPolicy resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + PipelineSourceCodePipelineActionRoleDefaultPolicy5FD830BF: { + Type: 'AWS::IAM::Policy', + Properties: { + PolicyDocument: { + Statement: [ + { + Action: [ + 's3:GetObject*', + 's3:GetBucket*', + 's3:List*', + 's3:DeleteObject*', + 's3:PutObject', + 's3:PutObjectLegalHold', + 's3:PutObjectRetention', + 's3:PutObjectTagging', + 's3:PutObjectVersionTagging', + 's3:Abort*', + ], + Effect: 'Allow', + Resource: [ + { + 'Fn::GetAtt': ['PipelineSecureBucketB3EEB324', 'Arn'], + }, + { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': ['PipelineSecureBucketB3EEB324', 'Arn'], + }, + '/*', + ], + ], + }, + ], + }, + { + Action: ['kms:Decrypt', 'kms:DescribeKey', 'kms:Encrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*'], + Effect: 'Allow', + Resource: { + Ref: 'SsmParameterValueacceleratorawsacceleratorinstallerkmskeyarnC96584B6F00A464EAD1953AFF4B05118Parameter', + }, + }, + { + Action: [ + 'codecommit:GetBranch', + 'codecommit:GetCommit', + 'codecommit:UploadArchive', + 'codecommit:GetUploadArchiveStatus', + 'codecommit:CancelUploadArchive', + ], + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':codecommit:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':accelerator-source', + ], + ], + }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'PipelineSourceCodePipelineActionRoleDefaultPolicy5FD830BF', + Roles: [ + { + Ref: 'PipelineSourceCodePipelineActionRoleBBC58FD5', + }, + ], + }, + }, + }, + }); + }); + + /** + * PipelineSourceConfigurationCodePipelineActionRole configuration test + */ + test(`${testNamePrefix} PipelineSourceConfigurationCodePipelineActionRole resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + PipelineSourceConfigurationCodePipelineActionRoleA2807B19: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + AWS: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::', + { + Ref: 'AWS::AccountId', + }, + ':root', + ], + ], + }, + }, + }, + ], + Version: '2012-10-17', + }, + }, + }, + }, + }); + }); + + /** + * PipelineSourceConfigurationCodePipelineActionRoleDefaultPolicy configuration test + */ + test(`${testNamePrefix} PipelineSourceConfigurationCodePipelineActionRoleDefaultPolicy resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + PipelineSourceConfigurationCodePipelineActionRoleDefaultPolicy5FE1A228: { + Type: 'AWS::IAM::Policy', + Properties: { + PolicyDocument: { + Statement: [ + { + Action: [ + 's3:GetObject*', + 's3:GetBucket*', + 's3:List*', + 's3:DeleteObject*', + 's3:PutObject', + 's3:PutObjectLegalHold', + 's3:PutObjectRetention', + 's3:PutObjectTagging', + 's3:PutObjectVersionTagging', + 's3:Abort*', + ], + Effect: 'Allow', + Resource: [ + { + 'Fn::GetAtt': ['PipelineSecureBucketB3EEB324', 'Arn'], + }, + { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': ['PipelineSecureBucketB3EEB324', 'Arn'], + }, + '/*', + ], + ], + }, + ], + }, + { + Action: ['kms:Decrypt', 'kms:DescribeKey', 'kms:Encrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*'], + Effect: 'Allow', + Resource: { + Ref: 'SsmParameterValueacceleratorawsacceleratorinstallerkmskeyarnC96584B6F00A464EAD1953AFF4B05118Parameter', + }, + }, + { + Action: [ + 'codecommit:GetBranch', + 'codecommit:GetCommit', + 'codecommit:UploadArchive', + 'codecommit:GetUploadArchiveStatus', + 'codecommit:CancelUploadArchive', + ], + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': ['PipelineConfigRepositoryE5225086', 'Arn'], + }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'PipelineSourceConfigurationCodePipelineActionRoleDefaultPolicy5FE1A228', + Roles: [ + { + Ref: 'PipelineSourceConfigurationCodePipelineActionRoleA2807B19', + }, + ], + }, + }, + }, + }); + }); + + /** + * PipelineToolkitProject resource configuration test + */ + test(`${testNamePrefix} PipelineToolkitProject resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + PipelineToolkitProjectBCBD6910: { + Type: 'AWS::CodeBuild::Project', + Properties: { + Artifacts: { + Type: 'CODEPIPELINE', + }, + Environment: { + ComputeType: 'BUILD_GENERAL1_MEDIUM', + EnvironmentVariables: [ + { + Name: 'NODE_OPTIONS', + Type: 'PLAINTEXT', + Value: '--max_old_space_size=4096', + }, + { + Name: 'CDK_NEW_BOOTSTRAP', + Type: 'PLAINTEXT', + Value: '1', + }, + { + Name: 'ACCELERATOR_QUALIFIER', + Type: 'PLAINTEXT', + Value: 'aws-accelerator', + }, + ], + Image: 'aws/codebuild/standard:5.0', + ImagePullCredentialsType: 'CODEBUILD', + PrivilegedMode: true, + Type: 'LINUX_CONTAINER', + }, + ServiceRole: { + 'Fn::GetAtt': ['AdminCdkToolkitRole292E163A', 'Arn'], + }, + Source: { + BuildSpec: { + 'Fn::Join': [ + '', + [ + '{\n "version": "0.2",\n "phases": {\n "install": {\n "runtime-versions": {\n "nodejs": 14\n }\n },\n "build": {\n "commands": [\n "env",\n "cd source",\n "cd packages/@aws-accelerator/accelerator",\n "if [ -z \\"${ACCELERATOR_STAGE}\\" ]; then yarn run ts-node --transpile-only cdk.ts synth --require-approval never --config-dir $CODEBUILD_SRC_DIR_Config --partition ', + { + Ref: 'AWS::Partition', + }, + '; fi",\n "if [ ! -z \\"${ACCELERATOR_STAGE}\\" ]; then yarn run ts-node --transpile-only cdk.ts synth --stage $ACCELERATOR_STAGE --require-approval never --config-dir $CODEBUILD_SRC_DIR_Config --partition ', + { + Ref: 'AWS::Partition', + }, + '; fi",\n "yarn run ts-node --transpile-only cdk.ts --require-approval never $CDK_OPTIONS --config-dir $CODEBUILD_SRC_DIR_Config --partition ', + { + Ref: 'AWS::Partition', + }, + ' --app cdk.out"\n ]\n }\n }\n}', + ], + ], + }, + Type: 'CODEPIPELINE', + }, + }, + }, + }, + }); + }); + + /** + * PipelineToolkitRole resource configuration test + */ + test(`${testNamePrefix} PipelineToolkitRole resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + AdminCdkToolkitRole292E163A: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'codebuild.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::aws:policy/AdministratorAccess', + ], + ], + }, + ], + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/accelerator/test/security-audit-stack.test.ts b/source/packages/@aws-accelerator/accelerator/test/security-audit-stack.test.ts new file mode 100644 index 000000000..d6c3afac6 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/test/security-audit-stack.test.ts @@ -0,0 +1,1477 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { + ACCOUNT_CONFIG, + GLOBAL_CONFIG, + IAM_CONFIG, + NETWORK_CONFIG, + ORGANIZATION_CONFIG, + SECURITY_CONFIG, +} from './configs/test-config'; +import * as cdk from 'aws-cdk-lib'; +import { SecurityAuditStack } from '../lib/stacks/security-audit-stack'; +import { AcceleratorStackNames } from '../lib/accelerator'; +import { AcceleratorStage } from '../lib/accelerator-stage'; +import * as path from 'path'; +import { AcceleratorStackProps } from '../lib/stacks/accelerator-stack'; + +const testNamePrefix = 'Construct(SecurityAuditStack): '; + +/** + * SecurityAuditStack + */ +const app = new cdk.App({ + context: { 'config-dir': path.join(__dirname, 'configs') }, +}); +const configDirPath = app.node.tryGetContext('config-dir'); + +const env = { + account: '333333333333', + region: 'us-east-1', +}; + +const props: AcceleratorStackProps = { + env, + configDirPath, + accountsConfig: ACCOUNT_CONFIG, + globalConfig: GLOBAL_CONFIG, + iamConfig: IAM_CONFIG, + networkConfig: NETWORK_CONFIG, + organizationConfig: ORGANIZATION_CONFIG, + securityConfig: SECURITY_CONFIG, + partition: 'aws', +}; + +const stack = new SecurityAuditStack( + app, + `${AcceleratorStackNames[AcceleratorStage.SECURITY_AUDIT]}-${env.account}-${env.region}`, + props, +); + +/** + * SecurityAuditStack construct test + */ +describe('SecurityAuditStack', () => { + /** + * Number of S3 bucket resource test + */ + test(`${testNamePrefix} S3 bucket resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::S3::Bucket', 2); + }); + + /** + * Number of S3 bucket policy resource test + */ + test(`${testNamePrefix} S3 bucket policy resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::S3::BucketPolicy', 2); + }); + + /** + * Number of Lambda function resource test + */ + test(`${testNamePrefix} Lambda function resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 6); + }); + + /** + * Number of IAM role resource test + */ + test(`${testNamePrefix} IAM role resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 6); + }); + + /** + * Number of SNS topic resource test + */ + test(`${testNamePrefix} SNS topic resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::SNS::Topic', 3); + }); + + /** + * Number of SNS topic policy resource test + */ + test(`${testNamePrefix} SNS topic policy resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::SNS::TopicPolicy', 3); + }); + + /** + * Number of SNS subscription resource test + */ + test(`${testNamePrefix} SNS subscription resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::SNS::Subscription', 3); + }); + + /** + * Number of SNS subscription resource test + */ + test(`${testNamePrefix} SNS subscription resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::SNS::Subscription', 3); + }); + + /** + * Number of GuardDutyUpdateDetector custom resource test + */ + test(`${testNamePrefix} GuardDutyUpdateDetector custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::GuardDutyUpdateDetector', 1); + }); + + /** + * Number of GuardDutyCreateMembers custom resource test + */ + test(`${testNamePrefix} GuardDutyCreateMembers custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::GuardDutyCreateMembers', 1); + }); + + /** + * Number of GuardDutyCreateMembers custom resource test + */ + test(`${testNamePrefix} GuardDutyCreateMembers custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::GuardDutyCreateMembers', 1); + }); + + /** + * Number of MacieCreateMember custom resource test + */ + test(`${testNamePrefix} MacieCreateMember custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::MacieCreateMember', 1); + }); + + /** + * Number of MacieCreateMember custom resource test + */ + test(`${testNamePrefix} MacieCreateMember custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::MacieCreateMember', 1); + }); + + /** + * Number of DescribeOrganization custom resource test + */ + test(`${testNamePrefix} DescribeOrganization custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::DescribeOrganization', 1); + }); + + /** + * Number of SecurityHubCreateMembers custom resource test + */ + test(`${testNamePrefix} SecurityHubCreateMembers custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::SecurityHubCreateMembers', 1); + }); + + /** + * AccessAnalyzer Analyzer resource configuration test + */ + test(`${testNamePrefix} AccessAnalyzer Analyzer resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + AccessAnalyzer: { + Type: 'AWS::AccessAnalyzer::Analyzer', + Properties: { + Type: 'ORGANIZATION', + }, + }, + }, + }); + }); + + /** + * CustomGuardDutyCreateMembersCustomResourceProviderHandler resource configuration test + */ + test(`${testNamePrefix} CustomGuardDutyCreateMembersCustomResourceProviderHandler resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomGuardDutyCreateMembersCustomResourceProviderHandler0A16C673: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomGuardDutyCreateMembersCustomResourceProviderRole2D82020E'], + Properties: { + Code: { + S3Bucket: 'cdk-hnb659fds-assets-333333333333-us-east-1', + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomGuardDutyCreateMembersCustomResourceProviderRole2D82020E', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * CustomGuardDutyCreateMembersCustomResourceProviderRole resource configuration test + */ + // test(`${testNamePrefix} CustomGuardDutyCreateMembersCustomResourceProviderRole resource configuration test`, () => { + // cdk.assertions.Template.fromStack(stack).templateMatches({ + // Resources: { + // CustomGuardDutyCreateMembersCustomResourceProviderRole2D82020E: { + // Type: 'AWS::IAM::Role', + // Properties: { + // AssumeRolePolicyDocument: { + // Statement: [ + // { + // Action: 'sts:AssumeRole', + // Effect: 'Allow', + // Principal: { + // Service: 'lambda.amazonaws.com', + // }, + // }, + // ], + // Version: '2012-10-17', + // }, + // ManagedPolicyArns: [ + // { + // 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + // }, + // ], + // Policies: [ + // { + // PolicyDocument: { + // Statement: [ + // { + // Action: ['organizations:ListAccounts'], + // Condition: { + // StringLikeIfExists: { + // 'organizations:ListAccounts': ['guardduty.amazonaws.com'], + // }, + // }, + // Effect: 'Allow', + // Resource: '*', + // Sid: 'GuardDutyCreateMembersTaskOrganizationAction', + // }, + // { + // Action: [ + // 'guardDuty:ListDetectors', + // 'guardDuty:ListOrganizationAdminAccounts', + // 'guardDuty:UpdateOrganizationConfiguration', + // 'guardduty:CreateMembers', + // 'guardduty:DeleteMembers', + // 'guardduty:DisassociateMembers', + // 'guardduty:ListDetectors', + // 'guardduty:ListMembers', + // ], + // Effect: 'Allow', + // Resource: '*', + // Sid: 'GuardDutyCreateMembersTaskGuardDutyActions', + // }, + // ], + // Version: '2012-10-17', + // }, + // PolicyName: 'Inline', + // }, + // ], + // }, + // }, + // }, + // }); + // }); + + /** + * CustomGuardDutyUpdateDetectorCustomResourceProviderHandler resource configuration test + */ + test(`${testNamePrefix} CustomGuardDutyUpdateDetectorCustomResourceProviderHandler resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomGuardDutyUpdateDetectorCustomResourceProviderHandler78DF0FF9: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomGuardDutyUpdateDetectorCustomResourceProviderRole3014073E'], + Properties: { + Code: { + S3Bucket: 'cdk-hnb659fds-assets-333333333333-us-east-1', + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomGuardDutyUpdateDetectorCustomResourceProviderRole3014073E', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * CustomGuardDutyUpdateDetectorCustomResourceProviderRole resource configuration test + */ + test(`${testNamePrefix} CustomGuardDutyUpdateDetectorCustomResourceProviderRole resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomGuardDutyUpdateDetectorCustomResourceProviderRole3014073E: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: [ + 'guardduty:ListDetectors', + 'guardduty:ListMembers', + 'guardduty:UpdateDetector', + 'guardduty:UpdateMemberDetectors', + ], + Effect: 'Allow', + Resource: '*', + Sid: 'GuardDutyUpdateDetectorTaskGuardDutyActions', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * CustomMacieCreateMemberCustomResourceProviderHandler resource configuration test + */ + test(`${testNamePrefix} CustomMacieCreateMemberCustomResourceProviderHandler resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomMacieCreateMemberCustomResourceProviderHandler913F75DB: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomMacieCreateMemberCustomResourceProviderRole3E8977EE'], + Properties: { + Code: { + S3Bucket: 'cdk-hnb659fds-assets-333333333333-us-east-1', + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomMacieCreateMemberCustomResourceProviderRole3E8977EE', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * CustomMacieCreateMemberCustomResourceProviderRole resource configuration test + */ + test(`${testNamePrefix} CustomMacieCreateMemberCustomResourceProviderRole resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomMacieCreateMemberCustomResourceProviderRole3E8977EE: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: ['organizations:ListAccounts'], + Condition: { + StringLikeIfExists: { + 'organizations:ListAccounts': ['macie.amazonaws.com'], + }, + }, + Effect: 'Allow', + Resource: '*', + Sid: 'MacieCreateMemberTaskOrganizationAction', + }, + { + Action: [ + 'macie2:CreateMember', + 'macie2:DeleteMember', + 'macie2:DescribeOrganizationConfiguration', + 'macie2:DisassociateMember', + 'macie2:EnableMacie', + 'macie2:GetMacieSession', + 'macie2:ListMembers', + 'macie2:UpdateOrganizationConfiguration', + ], + Effect: 'Allow', + Resource: '*', + Sid: 'MacieCreateMemberTaskMacieActions', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * CustomOrganizationsDescribeOrganizationCustomResourceProviderHandler resource configuration test + */ + test(`${testNamePrefix} CustomOrganizationsDescribeOrganizationCustomResourceProviderHandler resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomOrganizationsDescribeOrganizationCustomResourceProviderHandler4C6F49D1: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomOrganizationsDescribeOrganizationCustomResourceProviderRole775854D5'], + Properties: { + Code: { + S3Bucket: 'cdk-hnb659fds-assets-333333333333-us-east-1', + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomOrganizationsDescribeOrganizationCustomResourceProviderRole775854D5', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * CustomOrganizationsDescribeOrganizationCustomResourceProviderRole resource configuration test + */ + test(`${testNamePrefix} CustomOrganizationsDescribeOrganizationCustomResourceProviderRole resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomOrganizationsDescribeOrganizationCustomResourceProviderRole775854D5: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: ['organizations:DescribeOrganization'], + Effect: 'Allow', + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * CustomSecurityHubCreateMembersCustomResourceProviderHandler resource configuration test + */ + test(`${testNamePrefix} CustomSecurityHubCreateMembersCustomResourceProviderHandler resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomSecurityHubCreateMembersCustomResourceProviderHandler31D82BF3: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomSecurityHubCreateMembersCustomResourceProviderRoleFD355CB6'], + Properties: { + Code: { + S3Bucket: 'cdk-hnb659fds-assets-333333333333-us-east-1', + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomSecurityHubCreateMembersCustomResourceProviderRoleFD355CB6', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * CustomSecurityHubCreateMembersCustomResourceProviderRole resource configuration test + */ + test(`${testNamePrefix} CustomSecurityHubCreateMembersCustomResourceProviderRole resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomSecurityHubCreateMembersCustomResourceProviderRoleFD355CB6: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: ['organizations:ListAccounts'], + Condition: { + StringLikeIfExists: { + 'organizations:ListAccounts': ['securityhub.amazonaws.com'], + }, + }, + Effect: 'Allow', + Resource: '*', + Sid: 'SecurityHubCreateMembersTaskOrganizationAction', + }, + { + Action: [ + 'securityhub:CreateMembers', + 'securityhub:DeleteMembers', + 'securityhub:DisassociateMembers', + 'securityhub:EnableSecurityHub', + 'securityhub:ListMembers', + 'securityhub:UpdateOrganizationConfiguration', + ], + Effect: 'Allow', + Resource: '*', + Sid: 'SecurityHubCreateMembersTaskSecurityHubActions', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * GuardDutyDetectorConfig resource configuration test + */ + test(`${testNamePrefix} GuardDutyDetectorConfig resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + GuardDutyDetectorConfigDD64B103: { + Type: 'Custom::GuardDutyUpdateDetector', + UpdateReplacePolicy: 'Delete', + DeletionPolicy: 'Delete', + DependsOn: [ + 'CustomGuardDutyUpdateDetectorCustomResourceProviderLogGroup0E4B1900', + 'GuardDutyMembersD34CA003', + ], + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomGuardDutyUpdateDetectorCustomResourceProviderHandler78DF0FF9', 'Arn'], + }, + exportDestination: 's3', + exportFrequency: 'FIFTEEN_MINUTES', + isExportConfigEnable: true, + region: 'us-east-1', + }, + }, + }, + }); + }); + + /** + * GuardDutyMembers resource configuration test + */ + test(`${testNamePrefix} GuardDutyMembers resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + GuardDutyMembersD34CA003: { + Type: 'Custom::GuardDutyCreateMembers', + UpdateReplacePolicy: 'Delete', + DeletionPolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomGuardDutyCreateMembersCustomResourceProviderHandler0A16C673', 'Arn'], + }, + enableS3Protection: true, + region: 'us-east-1', + }, + }, + }, + }); + }); + + /** + * HighSnsTopic resource configuration test + */ + test(`${testNamePrefix} HighSnsTopic resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + HighSnsTopicF69104E5: { + Type: 'AWS::SNS::Topic', + Properties: { + DisplayName: 'AWS Accelerator - High Notifications', + KmsMasterKeyId: { + Ref: 'AcceleratorKeyLookup0C18DA36', + }, + TopicName: 'aws-accelerator-HighNotifications', + }, + }, + }, + }); + }); + + /** + * HighSnsTopicPolicy resource configuration test + */ + test(`${testNamePrefix} HighSnsTopicPolicy resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + HighSnsTopicPolicy59BE4137: { + Type: 'AWS::SNS::TopicPolicy', + Properties: { + PolicyDocument: { + Statement: [ + { + Action: 'sns:Publish', + Effect: 'Allow', + Principal: { + Service: 'cloudwatch.amazonaws.com', + }, + Resource: { + Ref: 'HighSnsTopicF69104E5', + }, + Sid: '0', + }, + { + Action: 'sns:Publish', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + Resource: { + Ref: 'HighSnsTopicF69104E5', + }, + Sid: '1', + }, + { + Action: 'sns:Publish', + Condition: { + StringEquals: { + 'aws:PrincipalOrgID': { + Ref: 'Organization29A5FC3F', + }, + }, + }, + Effect: 'Allow', + Principal: { + AWS: '*', + }, + Resource: { + Ref: 'HighSnsTopicF69104E5', + }, + Sid: '2', + }, + { + Action: ['sns:ListSubscriptionsByTopic', 'sns:ListTagsForResource', 'sns:GetTopicAttributes'], + Condition: { + StringEquals: { + 'aws:PrincipalOrgID': { + Ref: 'Organization29A5FC3F', + }, + }, + }, + Effect: 'Allow', + Principal: { + AWS: '*', + }, + Resource: { + Ref: 'HighSnsTopicF69104E5', + }, + Sid: 'Allow Organization list topic', + }, + ], + Version: '2012-10-17', + }, + Topics: [ + { + Ref: 'HighSnsTopicF69104E5', + }, + ], + }, + }, + }, + }); + }); + + /** + * HighSnsTopichighalertamazoncom resource configuration test + */ + test(`${testNamePrefix} HighSnsTopichighalertamazoncom resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + HighSnsTopichighalertamazoncom829BEACE: { + Type: 'AWS::SNS::Subscription', + Properties: { + Endpoint: 'highalert@amazon.com', + Protocol: 'email', + TopicArn: { + Ref: 'HighSnsTopicF69104E5', + }, + }, + }, + }, + }); + }); + + /** + * LowSnsTopic resource configuration test + */ + test(`${testNamePrefix} LowSnsTopic resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + LowSnsTopic53AD0F18: { + Type: 'AWS::SNS::Topic', + Properties: { + DisplayName: 'AWS Accelerator - Low Notifications', + KmsMasterKeyId: { + Ref: 'AcceleratorKeyLookup0C18DA36', + }, + TopicName: 'aws-accelerator-LowNotifications', + }, + }, + }, + }); + }); + + /** + * LowSnsTopicPolicy resource configuration test + */ + test(`${testNamePrefix} LowSnsTopicPolicy resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + LowSnsTopicPolicy0C1FEB12: { + Type: 'AWS::SNS::TopicPolicy', + Properties: { + PolicyDocument: { + Statement: [ + { + Action: 'sns:Publish', + Effect: 'Allow', + Principal: { + Service: 'cloudwatch.amazonaws.com', + }, + Resource: { + Ref: 'LowSnsTopic53AD0F18', + }, + Sid: '0', + }, + { + Action: 'sns:Publish', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + Resource: { + Ref: 'LowSnsTopic53AD0F18', + }, + Sid: '1', + }, + { + Action: 'sns:Publish', + Condition: { + StringEquals: { + 'aws:PrincipalOrgID': { + Ref: 'Organization29A5FC3F', + }, + }, + }, + Effect: 'Allow', + Principal: { + AWS: '*', + }, + Resource: { + Ref: 'LowSnsTopic53AD0F18', + }, + Sid: '2', + }, + { + Action: ['sns:ListSubscriptionsByTopic', 'sns:ListTagsForResource', 'sns:GetTopicAttributes'], + Condition: { + StringEquals: { + 'aws:PrincipalOrgID': { + Ref: 'Organization29A5FC3F', + }, + }, + }, + Effect: 'Allow', + Principal: { + AWS: '*', + }, + Resource: { + Ref: 'LowSnsTopic53AD0F18', + }, + Sid: 'Allow Organization list topic', + }, + ], + Version: '2012-10-17', + }, + Topics: [ + { + Ref: 'LowSnsTopic53AD0F18', + }, + ], + }, + }, + }, + }); + }); + + /** + * LowSnsTopiclowalertamazoncom resource configuration test + */ + test(`${testNamePrefix} LowSnsTopiclowalertamazoncom resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + LowSnsTopiclowalertamazoncom68C4704C: { + Type: 'AWS::SNS::Subscription', + Properties: { + Endpoint: 'lowalert@amazon.com', + Protocol: 'email', + TopicArn: { + Ref: 'LowSnsTopic53AD0F18', + }, + }, + }, + }, + }); + }); + + /** + * MacieMembers resource configuration test + */ + test(`${testNamePrefix} MacieMembers resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + MacieMembers1B6840B4: { + Type: 'Custom::MacieCreateMember', + UpdateReplacePolicy: 'Delete', + DeletionPolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomMacieCreateMemberCustomResourceProviderHandler913F75DB', 'Arn'], + }, + adminAccountId: '333333333333', + region: 'us-east-1', + }, + }, + }, + }); + }); + + /** + * MediumSnsTopic resource configuration test + */ + test(`${testNamePrefix} MediumSnsTopic resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + MediumSnsTopic267CAB5B: { + Type: 'AWS::SNS::Topic', + Properties: { + DisplayName: 'AWS Accelerator - Medium Notifications', + KmsMasterKeyId: { + Ref: 'AcceleratorKeyLookup0C18DA36', + }, + TopicName: 'aws-accelerator-MediumNotifications', + }, + }, + }, + }); + }); + + /** + * MediumSnsTopicPolicy resource configuration test + */ + test(`${testNamePrefix} MediumSnsTopicPolicy resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + MediumSnsTopicPolicy0B54F62B: { + Type: 'AWS::SNS::TopicPolicy', + Properties: { + PolicyDocument: { + Statement: [ + { + Action: 'sns:Publish', + Effect: 'Allow', + Principal: { + Service: 'cloudwatch.amazonaws.com', + }, + Resource: { + Ref: 'MediumSnsTopic267CAB5B', + }, + Sid: '0', + }, + { + Action: 'sns:Publish', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + Resource: { + Ref: 'MediumSnsTopic267CAB5B', + }, + Sid: '1', + }, + { + Action: 'sns:Publish', + Condition: { + StringEquals: { + 'aws:PrincipalOrgID': { + Ref: 'Organization29A5FC3F', + }, + }, + }, + Effect: 'Allow', + Principal: { + AWS: '*', + }, + Resource: { + Ref: 'MediumSnsTopic267CAB5B', + }, + Sid: '2', + }, + { + Action: ['sns:ListSubscriptionsByTopic', 'sns:ListTagsForResource', 'sns:GetTopicAttributes'], + Condition: { + StringEquals: { + 'aws:PrincipalOrgID': { + Ref: 'Organization29A5FC3F', + }, + }, + }, + Effect: 'Allow', + Principal: { + AWS: '*', + }, + Resource: { + Ref: 'MediumSnsTopic267CAB5B', + }, + Sid: 'Allow Organization list topic', + }, + ], + Version: '2012-10-17', + }, + Topics: [ + { + Ref: 'MediumSnsTopic267CAB5B', + }, + ], + }, + }, + }, + }); + }); + + /** + * MediumSnsTopicmidalertamazoncom resource configuration test + */ + test(`${testNamePrefix} MediumSnsTopicmidalertamazoncom resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + MediumSnsTopicmidalertamazoncom73D2DD2D: { + Type: 'AWS::SNS::Subscription', + Properties: { + Endpoint: 'midalert@amazon.com', + Protocol: 'email', + TopicArn: { + Ref: 'MediumSnsTopic267CAB5B', + }, + }, + }, + }, + }); + }); + + /** + * Organization custom resource configuration test + */ + test(`${testNamePrefix} Organization custom resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + Organization29A5FC3F: { + Type: 'Custom::DescribeOrganization', + UpdateReplacePolicy: 'Delete', + DeletionPolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomOrganizationsDescribeOrganizationCustomResourceProviderHandler4C6F49D1', 'Arn'], + }, + }, + }, + }, + }); + }); + + /** + * SecurityHubMembers resource configuration test + */ + test(`${testNamePrefix} SecurityHubMembers resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + SecurityHubMembers2A2B77C4: { + Type: 'Custom::SecurityHubCreateMembers', + UpdateReplacePolicy: 'Delete', + DeletionPolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomSecurityHubCreateMembersCustomResourceProviderHandler31D82BF3', 'Arn'], + }, + region: 'us-east-1', + }, + }, + }, + }); + }); + + /** + * AwsMacieExportConfigBucket resource configuration test + */ + test(`${testNamePrefix} AwsMacieExportConfigBucket resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + AwsMacieExportConfigBucket83E4FE4E: { + Type: 'AWS::S3::Bucket', + UpdateReplacePolicy: 'Retain', + DeletionPolicy: 'Retain', + Properties: { + BucketEncryption: { + ServerSideEncryptionConfiguration: [ + { + ServerSideEncryptionByDefault: { + KMSMasterKeyID: { + Ref: 'AcceleratorKeyLookup0C18DA36', + }, + SSEAlgorithm: 'aws:kms', + }, + }, + ], + }, + BucketName: { + 'Fn::Join': [ + '', + [ + 'aws-accelerator-org-macie-disc-repo-', + { + Ref: 'AWS::AccountId', + }, + '-', + { + Ref: 'AWS::Region', + }, + ], + ], + }, + OwnershipControls: { + Rules: [ + { + ObjectOwnership: 'BucketOwnerPreferred', + }, + ], + }, + PublicAccessBlockConfiguration: { + BlockPublicAcls: true, + BlockPublicPolicy: true, + IgnorePublicAcls: true, + RestrictPublicBuckets: true, + }, + Tags: [ + { + Key: 'aws-cdk:auto-macie-access-bucket', + Value: 'true', + }, + ], + VersioningConfiguration: { + Status: 'Enabled', + }, + }, + }, + }, + }); + }); + + /** + * GuardDutyPublishingDestinationBucket resource configuration test + */ + test(`${testNamePrefix} GuardDutyPublishingDestinationBucket resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + GuardDutyPublishingDestinationBucket1AFF21BB: { + Type: 'AWS::S3::Bucket', + UpdateReplacePolicy: 'Retain', + DeletionPolicy: 'Retain', + Metadata: { + cdk_nag: { + rules_to_suppress: [ + { + id: 'AwsSolutions-S1', + reason: + 'GuardDutyPublishingDestinationBucket has server access logs disabled till the task for access logging completed.', + }, + ], + }, + }, + Properties: { + BucketEncryption: { + ServerSideEncryptionConfiguration: [ + { + ServerSideEncryptionByDefault: { + KMSMasterKeyID: { + Ref: 'AcceleratorKeyLookup0C18DA36', + }, + SSEAlgorithm: 'aws:kms', + }, + }, + ], + }, + BucketName: { + 'Fn::Join': [ + '', + [ + 'aws-accelerator-org-gduty-pub-dest-', + { + Ref: 'AWS::AccountId', + }, + '-', + { + Ref: 'AWS::Region', + }, + ], + ], + }, + OwnershipControls: { + Rules: [ + { + ObjectOwnership: 'BucketOwnerPreferred', + }, + ], + }, + PublicAccessBlockConfiguration: { + BlockPublicAcls: true, + BlockPublicPolicy: true, + IgnorePublicAcls: true, + RestrictPublicBuckets: true, + }, + Tags: [ + { + Key: 'aws-cdk:auto-guardduty-access-bucket', + Value: 'true', + }, + ], + VersioningConfiguration: { + Status: 'Enabled', + }, + }, + }, + }, + }); + }); + + /** + * GuardDutyPublishingDestinationBucketPolicy resource configuration test + */ + test(`${testNamePrefix} GuardDutyPublishingDestinationBucketPolicy resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + GuardDutyPublishingDestinationBucketPolicyAEFA499A: { + Type: 'AWS::S3::BucketPolicy', + Properties: { + Bucket: { + Ref: 'GuardDutyPublishingDestinationBucket1AFF21BB', + }, + PolicyDocument: { + Statement: [ + { + Action: 's3:*', + Condition: { + Bool: { + 'aws:SecureTransport': 'false', + }, + }, + Effect: 'Deny', + Principal: { + AWS: '*', + }, + Resource: [ + { + 'Fn::GetAtt': ['GuardDutyPublishingDestinationBucket1AFF21BB', 'Arn'], + }, + { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': ['GuardDutyPublishingDestinationBucket1AFF21BB', 'Arn'], + }, + '/*', + ], + ], + }, + ], + Sid: 'deny-insecure-connections', + }, + { + Action: [ + 's3:GetObject*', + 's3:GetBucket*', + 's3:List*', + 's3:DeleteObject*', + 's3:PutObject', + 's3:PutObjectLegalHold', + 's3:PutObjectRetention', + 's3:PutObjectTagging', + 's3:PutObjectVersionTagging', + 's3:Abort*', + ], + Effect: 'Allow', + Principal: { + Service: 'guardduty.amazonaws.com', + }, + Resource: [ + { + 'Fn::GetAtt': ['GuardDutyPublishingDestinationBucket1AFF21BB', 'Arn'], + }, + { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': ['GuardDutyPublishingDestinationBucket1AFF21BB', 'Arn'], + }, + '/*', + ], + ], + }, + ], + }, + { + Action: ['s3:GetBucketLocation', 's3:PutObject'], + Condition: { + StringEquals: { + 'aws:PrincipalOrgID': { + Ref: 'Organization29A5FC3F', + }, + }, + }, + Effect: 'Allow', + Principal: { + AWS: '*', + }, + Resource: [ + { + 'Fn::GetAtt': ['GuardDutyPublishingDestinationBucket1AFF21BB', 'Arn'], + }, + { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': ['GuardDutyPublishingDestinationBucket1AFF21BB', 'Arn'], + }, + '/*', + ], + ], + }, + ], + Sid: 'Allow Organization principals to use of the bucket', + }, + ], + Version: '2012-10-17', + }, + }, + }, + }, + }); + }); + + /** + * AwsMacieExportConfigBucketPolicy resource configuration test + */ + test(`${testNamePrefix} AwsMacieExportConfigBucketPolicy resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + AwsMacieExportConfigBucketPolicy2C40E2D4: { + Type: 'AWS::S3::BucketPolicy', + Properties: { + Bucket: { + Ref: 'AwsMacieExportConfigBucket83E4FE4E', + }, + PolicyDocument: { + Statement: [ + { + Action: 's3:*', + Condition: { + Bool: { + 'aws:SecureTransport': 'false', + }, + }, + Effect: 'Deny', + Principal: { + AWS: '*', + }, + Resource: [ + { + 'Fn::GetAtt': ['AwsMacieExportConfigBucket83E4FE4E', 'Arn'], + }, + { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': ['AwsMacieExportConfigBucket83E4FE4E', 'Arn'], + }, + '/*', + ], + ], + }, + ], + Sid: 'deny-insecure-connections', + }, + { + Action: [ + 's3:GetObject*', + 's3:GetBucket*', + 's3:List*', + 's3:DeleteObject*', + 's3:PutObject', + 's3:PutObjectLegalHold', + 's3:PutObjectRetention', + 's3:PutObjectTagging', + 's3:PutObjectVersionTagging', + 's3:Abort*', + ], + Effect: 'Allow', + Principal: { + Service: 'macie.amazonaws.com', + }, + Resource: [ + { + 'Fn::GetAtt': ['AwsMacieExportConfigBucket83E4FE4E', 'Arn'], + }, + { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': ['AwsMacieExportConfigBucket83E4FE4E', 'Arn'], + }, + '/*', + ], + ], + }, + ], + }, + { + Action: ['s3:GetBucketLocation', 's3:PutObject'], + Condition: { + StringEquals: { + 'aws:PrincipalOrgID': { + Ref: 'Organization29A5FC3F', + }, + }, + }, + Effect: 'Allow', + Principal: { + AWS: '*', + }, + Resource: [ + { + 'Fn::GetAtt': ['AwsMacieExportConfigBucket83E4FE4E', 'Arn'], + }, + { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': ['AwsMacieExportConfigBucket83E4FE4E', 'Arn'], + }, + '/*', + ], + ], + }, + ], + Sid: 'Allow Organization principals to use of the bucket', + }, + ], + Version: '2012-10-17', + }, + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/accelerator/test/security-stack.test.ts b/source/packages/@aws-accelerator/accelerator/test/security-stack.test.ts new file mode 100644 index 000000000..a30b3d40a --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/test/security-stack.test.ts @@ -0,0 +1,590 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { SecurityStack } from '../lib/stacks/security-stack'; +import { AcceleratorStackNames } from '../lib/accelerator'; +import { AcceleratorStage } from '../lib/accelerator-stage'; +import { + ACCOUNT_CONFIG, + GLOBAL_CONFIG, + IAM_CONFIG, + NETWORK_CONFIG, + ORGANIZATION_CONFIG, + SECURITY_CONFIG, +} from './configs/test-config'; +import * as path from 'path'; +import { AcceleratorStackProps } from '../lib/stacks/accelerator-stack'; + +const testNamePrefix = 'Construct(SecurityStack): '; + +/** + * SecurityStack + */ +const app = new cdk.App({ + context: { 'config-dir': path.join(__dirname, 'configs') }, +}); +const configDirPath = app.node.tryGetContext('config-dir'); + +const env = { + account: '333333333333', + region: 'us-east-1', +}; + +const props: AcceleratorStackProps = { + env, + configDirPath, + accountsConfig: ACCOUNT_CONFIG, + globalConfig: GLOBAL_CONFIG, + iamConfig: IAM_CONFIG, + networkConfig: NETWORK_CONFIG, + organizationConfig: ORGANIZATION_CONFIG, + securityConfig: SECURITY_CONFIG, + partition: 'aws', +}; + +const stack = new SecurityStack( + app, + `${AcceleratorStackNames[AcceleratorStage.SECURITY]}-${env.account}-${env.region}`, + props, +); + +/** + * SecurityStack construct test + */ +describe('SecurityStack', () => { + /** + * Snapshot test + */ + //test(`${testNamePrefix} Snapshot Test`, () => { + // expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); + //}); + + /** + * Number of MaciePutClassificationExportConfiguration custom resource test + */ + test(`${testNamePrefix} MaciePutClassificationExportConfiguration custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::MaciePutClassificationExportConfiguration', 1); + }); + + /** + * Number of Lambda Function resource test + */ + test(`${testNamePrefix} Lambda Function resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 6); + }); + + /** + * Number of IAM Role resource test + */ + test(`${testNamePrefix} IAM Role resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 6); + }); + + /** + * Number of GuardDutyCreatePublishingDestinationCommand custom resource test + */ + test(`${testNamePrefix} GuardDutyCreatePublishingDestinationCommand custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::GuardDutyCreatePublishingDestinationCommand', 1); + }); + + /** + * Number of IamUpdateAccountPasswordPolicy custom resource test + */ + test(`${testNamePrefix} IamUpdateAccountPasswordPolicy custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::IamUpdateAccountPasswordPolicy', 1); + }); + + /** + * Number of SecurityHubBatchEnableStandards custom resource test + */ + test(`${testNamePrefix} SecurityHubBatchEnableStandards custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::SecurityHubBatchEnableStandards', 1); + }); + + /** + * Number of SecurityHubBatchEnableStandards custom resource test + */ + test(`${testNamePrefix} SecurityHubBatchEnableStandards custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::SecurityHubBatchEnableStandards', 1); + }); + + /** + * AwsMacieUpdateExportConfigClassification resource configuration test + */ + test(`${testNamePrefix} AwsMacieUpdateExportConfigClassification resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + AwsMacieUpdateExportConfigClassification832781E3: { + Type: 'Custom::MaciePutClassificationExportConfiguration', + UpdateReplacePolicy: 'Delete', + DeletionPolicy: 'Delete', + DependsOn: ['CustomMaciePutClassificationExportConfigurationCustomResourceProviderLogGroup727354F4'], + Properties: { + ServiceToken: { + 'Fn::GetAtt': [ + 'CustomMaciePutClassificationExportConfigurationCustomResourceProviderHandlerC53E2FCC', + 'Arn', + ], + }, + bucketName: { + 'Fn::Join': [ + '', + [ + 'aws-accelerator-org-macie-disc-repo-222222222222-', + { + Ref: 'AWS::Region', + }, + ], + ], + }, + keyPrefix: '333333333333-aws-macie-export-config', + kmsKeyArn: { + Ref: 'AcceleratorKeyLookup0C18DA36', + }, + region: 'us-east-1', + }, + }, + }, + }); + }); + + /** + * CustomGuardDutyCreatePublishingDestinationCommandCustomResourceProviderHandler resource configuration test + */ + test(`${testNamePrefix} CustomGuardDutyCreatePublishingDestinationCommandCustomResourceProviderHandler resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomGuardDutyCreatePublishingDestinationCommandCustomResourceProviderHandlerB3AE4CE8: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomGuardDutyCreatePublishingDestinationCommandCustomResourceProviderRoleD01DD26B'], + Properties: { + Code: { + S3Bucket: 'cdk-hnb659fds-assets-333333333333-us-east-1', + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': [ + 'CustomGuardDutyCreatePublishingDestinationCommandCustomResourceProviderRoleD01DD26B', + 'Arn', + ], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * CustomGuardDutyCreatePublishingDestinationCommandCustomResourceProviderRole resource configuration test + */ + test(`${testNamePrefix} CustomGuardDutyCreatePublishingDestinationCommandCustomResourceProviderRole resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomGuardDutyCreatePublishingDestinationCommandCustomResourceProviderRoleD01DD26B: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: [ + 'guardDuty:CreateDetector', + 'guardDuty:CreatePublishingDestination', + 'guardDuty:DeletePublishingDestination', + 'guardDuty:ListDetectors', + 'guardDuty:ListPublishingDestinations', + 'iam:CreateServiceLinkedRole', + ], + Effect: 'Allow', + Resource: '*', + Sid: 'GuardDutyCreatePublishingDestinationCommandTaskGuardDutyActions', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * CustomIamUpdateAccountPasswordPolicyCustomResourceProviderHandler resource configuration test + */ + test(`${testNamePrefix} CustomIamUpdateAccountPasswordPolicyCustomResourceProviderHandler resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomIamUpdateAccountPasswordPolicyCustomResourceProviderHandler63EDC7F4: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomIamUpdateAccountPasswordPolicyCustomResourceProviderRoleC4ECAFE0'], + Properties: { + Code: { + S3Bucket: 'cdk-hnb659fds-assets-333333333333-us-east-1', + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomIamUpdateAccountPasswordPolicyCustomResourceProviderRoleC4ECAFE0', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * CustomIamUpdateAccountPasswordPolicyCustomResourceProviderRole resource configuration test + */ + test(`${testNamePrefix} CustomIamUpdateAccountPasswordPolicyCustomResourceProviderRole resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomIamUpdateAccountPasswordPolicyCustomResourceProviderRoleC4ECAFE0: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: ['iam:UpdateAccountPasswordPolicy'], + Effect: 'Allow', + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * CustomMaciePutClassificationExportConfigurationCustomResourceProviderHandler resource configuration test + */ + test(`${testNamePrefix} CustomMaciePutClassificationExportConfigurationCustomResourceProviderHandler resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomMaciePutClassificationExportConfigurationCustomResourceProviderHandlerC53E2FCC: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomMaciePutClassificationExportConfigurationCustomResourceProviderRoleEB42D531'], + Properties: { + Code: { + S3Bucket: 'cdk-hnb659fds-assets-333333333333-us-east-1', + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': [ + 'CustomMaciePutClassificationExportConfigurationCustomResourceProviderRoleEB42D531', + 'Arn', + ], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * CustomMaciePutClassificationExportConfigurationCustomResourceProviderRole resource configuration test + */ + test(`${testNamePrefix} CustomMaciePutClassificationExportConfigurationCustomResourceProviderRole resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomMaciePutClassificationExportConfigurationCustomResourceProviderRoleEB42D531: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: [ + 'macie2:EnableMacie', + 'macie2:GetClassificationExportConfiguration', + 'macie2:GetMacieSession', + 'macie2:PutClassificationExportConfiguration', + ], + Effect: 'Allow', + Resource: '*', + Sid: 'MaciePutClassificationExportConfigurationTaskMacieActions', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * CustomSecurityHubBatchEnableStandardsCustomResourceProviderHandler resource configuration test + */ + test(`${testNamePrefix} CustomSecurityHubBatchEnableStandardsCustomResourceProviderHandler resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomSecurityHubBatchEnableStandardsCustomResourceProviderHandler4BE622C1: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomSecurityHubBatchEnableStandardsCustomResourceProviderRole1ABC8ED2'], + Properties: { + Code: { + S3Bucket: 'cdk-hnb659fds-assets-333333333333-us-east-1', + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomSecurityHubBatchEnableStandardsCustomResourceProviderRole1ABC8ED2', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * CustomSecurityHubBatchEnableStandardsCustomResourceProviderRole resource configuration test + */ + test(`${testNamePrefix} CustomSecurityHubBatchEnableStandardsCustomResourceProviderRole resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomSecurityHubBatchEnableStandardsCustomResourceProviderRole1ABC8ED2: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: [ + 'securityhub:BatchDisableStandards', + 'securityhub:BatchEnableStandards', + 'securityhub:DescribeStandards', + 'securityhub:DescribeStandardsControls', + 'securityhub:EnableSecurityHub', + 'securityhub:GetEnabledStandards', + 'securityhub:UpdateStandardsControl', + ], + Effect: 'Allow', + Resource: '*', + Sid: 'SecurityHubCreateMembersTaskSecurityHubActions', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * GuardDutyPublishingDestination resource configuration test + */ + test(`${testNamePrefix} GuardDutyPublishingDestination resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + GuardDutyPublishingDestination52AE4412: { + Type: 'Custom::GuardDutyCreatePublishingDestinationCommand', + UpdateReplacePolicy: 'Delete', + DeletionPolicy: 'Delete', + DependsOn: ['CustomGuardDutyCreatePublishingDestinationCommandCustomResourceProviderLogGroup118A06DB'], + Properties: { + ServiceToken: { + 'Fn::GetAtt': [ + 'CustomGuardDutyCreatePublishingDestinationCommandCustomResourceProviderHandlerB3AE4CE8', + 'Arn', + ], + }, + bucketArn: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':s3:::aws-accelerator-org-gduty-pub-dest-222222222222-us-east-1', + ], + ], + }, + exportDestinationType: 'S3', + kmsKeyArn: { + Ref: 'AcceleratorKeyLookup0C18DA36', + }, + region: 'us-east-1', + }, + }, + }, + }); + }); + + /** + * IamPasswordPolicy resource configuration test + */ + test(`${testNamePrefix} IamPasswordPolicy resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + IamPasswordPolicy7117FCDB: { + Type: 'Custom::IamUpdateAccountPasswordPolicy', + UpdateReplacePolicy: 'Delete', + DeletionPolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomIamUpdateAccountPasswordPolicyCustomResourceProviderHandler63EDC7F4', 'Arn'], + }, + allowUsersToChangePassword: true, + hardExpiry: false, + maxPasswordAge: 90, + minimumPasswordLength: 14, + passwordReusePrevention: 24, + requireLowercaseCharacters: true, + requireNumbers: true, + requireSymbols: true, + requireUppercaseCharacters: true, + }, + }, + }, + }); + }); + + /** + * SecurityHubStandards resource configuration test + */ + test(`${testNamePrefix} SecurityHubStandards resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + SecurityHubStandards294083BB: { + Type: 'Custom::SecurityHubBatchEnableStandards', + UpdateReplacePolicy: 'Delete', + DeletionPolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomSecurityHubBatchEnableStandardsCustomResourceProviderHandler4BE622C1', 'Arn'], + }, + region: 'us-east-1', + standards: [ + { + controlsToDisable: ['IAM.1', 'EC2.10', 'Lambda.4'], + enable: true, + name: 'AWS Foundational Security Best Practices v1.0.0', + }, + { + controlsToDisable: ['PCI.IAM.3', 'PCI.S3.3', 'PCI.EC2.3', 'PCI.Lambda.2'], + enable: true, + name: 'PCI DSS v3.2.1', + }, + { + controlsToDisable: ['CIS.1.20', 'CIS.1.22', 'CIS.2.6'], + enable: true, + name: 'CIS AWS Foundations Benchmark v1.2.0', + }, + ], + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/accelerator/test/tester-pipeline-stack.test.ts b/source/packages/@aws-accelerator/accelerator/test/tester-pipeline-stack.test.ts new file mode 100644 index 000000000..c776742a8 --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/test/tester-pipeline-stack.test.ts @@ -0,0 +1,1155 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { TesterPipelineStack } from '../lib/stacks/tester-pipeline-stack'; + +const testNamePrefix = 'Construct(TesterPipelineStack): '; + +/** + * TesterPipelineStack + */ +const app = new cdk.App(); +const stack = new TesterPipelineStack(app, 'TesterPipelineStack', { + sourceRepositoryName: 'accelerator-source', + sourceBranchName: 'main', + qualifier: 'aws-accelerator', + managementCrossAccountRoleName: 'AWSControlTowerExecution', + managementAccountId: app.account, + managementAccountRoleName: 'AcceleratorAccountAccessRole', +}); + +/** + * TesterPipelineStack construct test + */ +describe('TesterPipelineStack', () => { + /** + * Number of CodePipeline resource test + */ + test(`${testNamePrefix} CodePipeline resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::CodePipeline::Pipeline', 1); + }); + + /** + * Number of CodeCommit Repository resource test + */ + test(`${testNamePrefix} CodeCommit Repository resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::CodeCommit::Repository', 1); + }); + + /** + * Number of Pipeline cloudwatch events rules resource test + */ + test(`${testNamePrefix} Pipeline cloudwatch events rules resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Events::Rule', 1); + }); + + /** + * Number of IAM Role resource test + */ + test(`${testNamePrefix} IAM Role resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 5); + }); + + /** + * Number of IAM Policy resource test + */ + test(`${testNamePrefix} IAM Policy resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Policy', 5); + }); + + /** + * Number of S3 Bucket resource test + */ + test(`${testNamePrefix} S3 Bucket resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::S3::Bucket', 1); + }); + + /** + * Number of BucketPolicy resource test + */ + test(`${testNamePrefix} BucketPolicy resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::S3::BucketPolicy', 1); + }); + + /** + * Number of CodeBuild project resource test + */ + test(`${testNamePrefix} CodeBuild project resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::CodeBuild::Project', 1); + }); + + /** + * Pipeline resource configuration test + */ + test(`${testNamePrefix} CodePipeline resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TesterPipeline69BAAE53: { + Type: 'AWS::CodePipeline::Pipeline', + DependsOn: ['TesterPipelinePipelineRoleDefaultPolicyFC1B0BBB', 'TesterPipelinePipelineRoleBF82DB14'], + Properties: { + ArtifactStore: { + EncryptionKey: { + Id: { + Ref: 'SsmParameterValueacceleratorawsacceleratorinstallerkmskeyarnC96584B6F00A464EAD1953AFF4B05118Parameter', + }, + Type: 'KMS', + }, + Location: { + Ref: 'TesterPipelineSecureBucket8740FCE8', + }, + Type: 'S3', + }, + Name: 'aws-accelerator-tester-pipeline', + RoleArn: { + 'Fn::GetAtt': ['TesterPipelinePipelineRoleBF82DB14', 'Arn'], + }, + Stages: [ + { + Actions: [ + { + ActionTypeId: { + Category: 'Source', + Owner: 'AWS', + Provider: 'CodeCommit', + Version: '1', + }, + Configuration: { + BranchName: 'main', + PollForSourceChanges: false, + RepositoryName: 'accelerator-source', + }, + Name: 'Source', + OutputArtifacts: [ + { + Name: 'Source', + }, + ], + RoleArn: { + 'Fn::GetAtt': ['TesterPipelineSourceCodePipelineActionRole1C0E642C', 'Arn'], + }, + RunOrder: 1, + }, + { + ActionTypeId: { + Category: 'Source', + Owner: 'AWS', + Provider: 'CodeCommit', + Version: '1', + }, + Configuration: { + BranchName: 'main', + PollForSourceChanges: false, + RepositoryName: { + 'Fn::GetAtt': ['TesterPipelineConfigRepositoryC9B47F16', 'Name'], + }, + }, + Name: 'Configuration', + OutputArtifacts: [ + { + Name: 'Config', + }, + ], + RoleArn: { + 'Fn::GetAtt': ['TesterPipelineSourceConfigurationCodePipelineActionRole6DD3F86D', 'Arn'], + }, + RunOrder: 1, + }, + ], + Name: 'Source', + }, + { + Actions: [ + { + ActionTypeId: { + Category: 'Build', + Owner: 'AWS', + Provider: 'CodeBuild', + Version: '1', + }, + Configuration: { + PrimarySource: 'Source', + ProjectName: { + Ref: 'TesterPipelineTesterProject3BEC9F5A', + }, + }, + InputArtifacts: [ + { + Name: 'Source', + }, + { + Name: 'Config', + }, + ], + Name: 'Deploy', + OutputArtifacts: [ + { + Name: 'DeployOutput', + }, + ], + RoleArn: { + 'Fn::GetAtt': ['TesterPipelinePipelineRoleBF82DB14', 'Arn'], + }, + RunOrder: 1, + }, + ], + Name: 'Deploy', + }, + ], + }, + }, + }, + }); + }); + + /** + * Config CodeCommit repository resource configuration test + */ + test(`${testNamePrefix} Config CodeCommit repository resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TesterPipelineConfigRepositoryC9B47F16: { + Type: 'AWS::CodeCommit::Repository', + DeletionPolicy: 'Retain', + UpdateReplacePolicy: 'Retain', + Properties: { + Code: { + BranchName: 'main', + S3: { + Bucket: { + 'Fn::Sub': 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', + }, + }, + }, + RepositoryDescription: 'AWS Accelerator functional test configuration repository', + RepositoryName: 'aws-accelerator-test-config', + }, + }, + }, + }); + }); + + /** + * Config CodeCommit repository event rule configuration test + */ + test(`${testNamePrefix} Config CodeCommit repository event rule resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TesterPipelineConfigRepositoryTesterPipelineStackTesterPipeline463CB8B6mainEventRuleB1B7F3DD: { + Type: 'AWS::Events::Rule', + Properties: { + EventPattern: { + detail: { + event: ['referenceCreated', 'referenceUpdated'], + referenceName: ['main'], + }, + 'detail-type': ['CodeCommit Repository State Change'], + resources: [ + { + 'Fn::GetAtt': ['TesterPipelineConfigRepositoryC9B47F16', 'Arn'], + }, + ], + source: ['aws.codecommit'], + }, + State: 'ENABLED', + Targets: [ + { + Arn: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':codepipeline:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':', + { + Ref: 'TesterPipeline69BAAE53', + }, + ], + ], + }, + Id: 'Target0', + RoleArn: { + 'Fn::GetAtt': ['TesterPipelineEventsRoleC96AADF0', 'Arn'], + }, + }, + ], + }, + }, + }, + }); + }); + + /** + * CodePipeline deploy stage IAM role resource configuration test + */ + test(`${testNamePrefix} CodePipeline deploy stage IAM role resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TesterPipelineDeployAdminRole3DA8CFF7: { + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'codebuild.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::aws:policy/AdministratorAccess', + ], + ], + }, + ], + }, + }, + }, + }); + }); + + // /** + // * CodePipeline deploy stage IAM role default policy resource configuration test + // */ + // test(`${testNamePrefix} CodePipeline deploy stage IAM role default policy resource configuration test`, () => { + // cdk.assertions.Template.fromStack(stack).templateMatches({ + // Resources: { + // TesterPipelineDeployRoleDefaultPolicyBB88BBD9: { + // Type: 'AWS::IAM::Policy', + // Properties: { + // PolicyDocument: { + // Statement: [ + // { + // Action: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'], + // Effect: 'Allow', + // Resource: [ + // { + // 'Fn::Join': [ + // '', + // [ + // 'arn:', + // { + // Ref: 'AWS::Partition', + // }, + // ':logs:', + // { + // Ref: 'AWS::Region', + // }, + // ':', + // { + // Ref: 'AWS::AccountId', + // }, + // ':log-group:/aws/codebuild/', + // { + // Ref: 'TesterPipelineTesterProject3BEC9F5A', + // }, + // ], + // ], + // }, + // { + // 'Fn::Join': [ + // '', + // [ + // 'arn:', + // { + // Ref: 'AWS::Partition', + // }, + // ':logs:', + // { + // Ref: 'AWS::Region', + // }, + // ':', + // { + // Ref: 'AWS::AccountId', + // }, + // ':log-group:/aws/codebuild/', + // { + // Ref: 'TesterPipelineTesterProject3BEC9F5A', + // }, + // ':*', + // ], + // ], + // }, + // ], + // }, + // { + // Action: [ + // 'codebuild:CreateReportGroup', + // 'codebuild:CreateReport', + // 'codebuild:UpdateReport', + // 'codebuild:BatchPutTestCases', + // 'codebuild:BatchPutCodeCoverages', + // ], + // Effect: 'Allow', + // Resource: { + // 'Fn::Join': [ + // '', + // [ + // 'arn:', + // { + // Ref: 'AWS::Partition', + // }, + // ':codebuild:', + // { + // Ref: 'AWS::Region', + // }, + // ':', + // { + // Ref: 'AWS::AccountId', + // }, + // ':report-group/', + // { + // Ref: 'TesterPipelineTesterProject3BEC9F5A', + // }, + // '-*', + // ], + // ], + // }, + // }, + // { + // Action: ['kms:Decrypt', 'kms:Encrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*'], + // Effect: 'Allow', + // Resource: { + // Ref: 'SsmParameterValueacceleratorawsacceleratorinstallerkmskeyarnC96584B6F00A464EAD1953AFF4B05118Parameter', + // }, + // }, + // { + // Action: [ + // 's3:GetObject*', + // 's3:GetBucket*', + // 's3:List*', + // 's3:DeleteObject*', + // 's3:PutObject', + // 's3:PutObjectLegalHold', + // 's3:PutObjectRetention', + // 's3:PutObjectTagging', + // 's3:PutObjectVersionTagging', + // 's3:Abort*', + // ], + // Effect: 'Allow', + // Resource: [ + // { + // 'Fn::GetAtt': ['TesterPipelineSecureBucket8740FCE8', 'Arn'], + // }, + // { + // 'Fn::Join': [ + // '', + // [ + // { + // 'Fn::GetAtt': ['TesterPipelineSecureBucket8740FCE8', 'Arn'], + // }, + // '/*', + // ], + // ], + // }, + // ], + // }, + // { + // Action: ['kms:Decrypt', 'kms:DescribeKey', 'kms:Encrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*'], + // Effect: 'Allow', + // Resource: { + // Ref: 'SsmParameterValueacceleratorawsacceleratorinstallerkmskeyarnC96584B6F00A464EAD1953AFF4B05118Parameter', + // }, + // }, + // ], + // Version: '2012-10-17', + // }, + // PolicyName: 'TesterPipelineDeployRoleDefaultPolicyBB88BBD9', + // Roles: [ + // { + // Ref: 'TesterPipelineDeployAdminRole3DA8CFF7', + // }, + // ], + // }, + // }, + // }, + // }); + // }); + + /** + * CodePipeline config repository events role resource configuration test + */ + test(`${testNamePrefix} CodePipeline config repository events role resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TesterPipelineEventsRoleC96AADF0: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'events.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + }, + }, + }, + }); + }); + + /** + * CodePipeline config repository events role default policy resource configuration test + */ + test(`${testNamePrefix} CodePipeline config repository events role default policy resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TesterPipelineEventsRoleDefaultPolicy61DCBDBE: { + Type: 'AWS::IAM::Policy', + Properties: { + PolicyDocument: { + Statement: [ + { + Action: 'codepipeline:StartPipelineExecution', + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':codepipeline:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':', + { + Ref: 'TesterPipeline69BAAE53', + }, + ], + ], + }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'TesterPipelineEventsRoleDefaultPolicy61DCBDBE', + Roles: [ + { + Ref: 'TesterPipelineEventsRoleC96AADF0', + }, + ], + }, + }, + }, + }); + }); + + /** + * CodePipeline IAM role resource configuration test + */ + test(`${testNamePrefix} CodePipeline IAM role resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TesterPipelinePipelineRoleBF82DB14: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'codepipeline.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + }, + }, + }, + }); + }); + + /** + * CodePipeline IAM role default policy resource configuration test + */ + test(`${testNamePrefix} CodePipeline IAM role default policy resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TesterPipelinePipelineRoleDefaultPolicyFC1B0BBB: { + Type: 'AWS::IAM::Policy', + Metadata: { + cdk_nag: { + rules_to_suppress: [ + { + id: 'AwsSolutions-IAM5', + reason: 'PipelineRole DefaultPolicy is built by cdk.', + }, + ], + }, + }, + Properties: { + PolicyDocument: { + Statement: [ + { + Action: [ + 's3:GetObject*', + 's3:GetBucket*', + 's3:List*', + 's3:DeleteObject*', + 's3:PutObject', + 's3:PutObjectLegalHold', + 's3:PutObjectRetention', + 's3:PutObjectTagging', + 's3:PutObjectVersionTagging', + 's3:Abort*', + ], + Effect: 'Allow', + Resource: [ + { + 'Fn::GetAtt': ['TesterPipelineSecureBucket8740FCE8', 'Arn'], + }, + { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': ['TesterPipelineSecureBucket8740FCE8', 'Arn'], + }, + '/*', + ], + ], + }, + ], + }, + { + Action: ['kms:Decrypt', 'kms:DescribeKey', 'kms:Encrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*'], + Effect: 'Allow', + Resource: { + Ref: 'SsmParameterValueacceleratorawsacceleratorinstallerkmskeyarnC96584B6F00A464EAD1953AFF4B05118Parameter', + }, + }, + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': ['TesterPipelineSourceCodePipelineActionRole1C0E642C', 'Arn'], + }, + }, + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': ['TesterPipelineSourceConfigurationCodePipelineActionRole6DD3F86D', 'Arn'], + }, + }, + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': ['TesterPipelinePipelineRoleBF82DB14', 'Arn'], + }, + }, + { + Action: ['codebuild:BatchGetBuilds', 'codebuild:StartBuild', 'codebuild:StopBuild'], + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': ['TesterPipelineTesterProject3BEC9F5A', 'Arn'], + }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'TesterPipelinePipelineRoleDefaultPolicyFC1B0BBB', + Roles: [ + { + Ref: 'TesterPipelinePipelineRoleBF82DB14', + }, + ], + }, + }, + }, + }); + }); + /** + * CodePipeline config S3 bucket resource configuration test + */ + test(`${testNamePrefix} CodePipeline config S3 bucket resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TesterPipelineSecureBucket8740FCE8: { + Type: 'AWS::S3::Bucket', + DeletionPolicy: 'Retain', + UpdateReplacePolicy: 'Retain', + Properties: { + BucketEncryption: { + ServerSideEncryptionConfiguration: [ + { + ServerSideEncryptionByDefault: { + KMSMasterKeyID: { + Ref: 'SsmParameterValueacceleratorawsacceleratorinstallerkmskeyarnC96584B6F00A464EAD1953AFF4B05118Parameter', + }, + SSEAlgorithm: 'aws:kms', + }, + }, + ], + }, + BucketName: { + 'Fn::Join': [ + '', + [ + 'aws-accelerator-tester-', + { + Ref: 'AWS::AccountId', + }, + '-', + { + Ref: 'AWS::Region', + }, + ], + ], + }, + OwnershipControls: { + Rules: [ + { + ObjectOwnership: 'BucketOwnerPreferred', + }, + ], + }, + PublicAccessBlockConfiguration: { + BlockPublicAcls: true, + BlockPublicPolicy: true, + IgnorePublicAcls: true, + RestrictPublicBuckets: true, + }, + VersioningConfiguration: { + Status: 'Enabled', + }, + }, + }, + }, + }); + }); + + /** + * CodePipeline config S3 bucket policy resource configuration test + */ + test(`${testNamePrefix} CodePipeline config S3 bucket policy resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TesterPipelineSecureBucketPolicyD3292C3A: { + Type: 'AWS::S3::BucketPolicy', + Properties: { + Bucket: { + Ref: 'TesterPipelineSecureBucket8740FCE8', + }, + PolicyDocument: { + Statement: [ + { + Action: 's3:*', + Condition: { + Bool: { + 'aws:SecureTransport': 'false', + }, + }, + Effect: 'Deny', + Principal: { + AWS: '*', + }, + Resource: [ + { + 'Fn::GetAtt': ['TesterPipelineSecureBucket8740FCE8', 'Arn'], + }, + { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': ['TesterPipelineSecureBucket8740FCE8', 'Arn'], + }, + '/*', + ], + ], + }, + ], + Sid: 'deny-insecure-connections', + }, + ], + Version: '2012-10-17', + }, + }, + }, + }, + }); + }); + + /** + * CodePipeline source action IAM role resource configuration test + */ + test(`${testNamePrefix} CodePipeline source action IAM role resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TesterPipelineSourceCodePipelineActionRole1C0E642C: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + AWS: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::', + { + Ref: 'AWS::AccountId', + }, + ':root', + ], + ], + }, + }, + }, + ], + Version: '2012-10-17', + }, + }, + }, + }, + }); + }); + + /** + * CodePipeline source action IAM role default policy resource configuration test + */ + test(`${testNamePrefix} CodePipeline source action IAM role default policy resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TesterPipelineSourceCodePipelineActionRoleDefaultPolicy9AAA0DC1: { + Type: 'AWS::IAM::Policy', + Properties: { + PolicyDocument: { + Statement: [ + { + Action: [ + 's3:GetObject*', + 's3:GetBucket*', + 's3:List*', + 's3:DeleteObject*', + 's3:PutObject', + 's3:PutObjectLegalHold', + 's3:PutObjectRetention', + 's3:PutObjectTagging', + 's3:PutObjectVersionTagging', + 's3:Abort*', + ], + Effect: 'Allow', + Resource: [ + { + 'Fn::GetAtt': ['TesterPipelineSecureBucket8740FCE8', 'Arn'], + }, + { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': ['TesterPipelineSecureBucket8740FCE8', 'Arn'], + }, + '/*', + ], + ], + }, + ], + }, + { + Action: ['kms:Decrypt', 'kms:DescribeKey', 'kms:Encrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*'], + Effect: 'Allow', + Resource: { + Ref: 'SsmParameterValueacceleratorawsacceleratorinstallerkmskeyarnC96584B6F00A464EAD1953AFF4B05118Parameter', + }, + }, + { + Action: [ + 'codecommit:GetBranch', + 'codecommit:GetCommit', + 'codecommit:UploadArchive', + 'codecommit:GetUploadArchiveStatus', + 'codecommit:CancelUploadArchive', + ], + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':codecommit:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':accelerator-source', + ], + ], + }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'TesterPipelineSourceCodePipelineActionRoleDefaultPolicy9AAA0DC1', + Roles: [ + { + Ref: 'TesterPipelineSourceCodePipelineActionRole1C0E642C', + }, + ], + }, + }, + }, + }); + }); + + /** + * CodePipeline source configuration IAM role resource configuration test + */ + test(`${testNamePrefix} CodePipeline source configuration IAM role resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TesterPipelineSourceConfigurationCodePipelineActionRole6DD3F86D: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + AWS: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::', + { + Ref: 'AWS::AccountId', + }, + ':root', + ], + ], + }, + }, + }, + ], + Version: '2012-10-17', + }, + }, + }, + }, + }); + }); + + /** + * CodePipeline source configuration IAM role default policy resource configuration test + */ + test(`${testNamePrefix} CodePipeline source configuration IAM role default policy resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TesterPipelineSourceConfigurationCodePipelineActionRoleDefaultPolicyCD0DC6AA: { + Type: 'AWS::IAM::Policy', + Properties: { + PolicyDocument: { + Statement: [ + { + Action: [ + 's3:GetObject*', + 's3:GetBucket*', + 's3:List*', + 's3:DeleteObject*', + 's3:PutObject', + 's3:PutObjectLegalHold', + 's3:PutObjectRetention', + 's3:PutObjectTagging', + 's3:PutObjectVersionTagging', + 's3:Abort*', + ], + Effect: 'Allow', + Resource: [ + { + 'Fn::GetAtt': ['TesterPipelineSecureBucket8740FCE8', 'Arn'], + }, + { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': ['TesterPipelineSecureBucket8740FCE8', 'Arn'], + }, + '/*', + ], + ], + }, + ], + }, + { + Action: ['kms:Decrypt', 'kms:DescribeKey', 'kms:Encrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*'], + Effect: 'Allow', + Resource: { + Ref: 'SsmParameterValueacceleratorawsacceleratorinstallerkmskeyarnC96584B6F00A464EAD1953AFF4B05118Parameter', + }, + }, + { + Action: [ + 'codecommit:GetBranch', + 'codecommit:GetCommit', + 'codecommit:UploadArchive', + 'codecommit:GetUploadArchiveStatus', + 'codecommit:CancelUploadArchive', + ], + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': ['TesterPipelineConfigRepositoryC9B47F16', 'Arn'], + }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'TesterPipelineSourceConfigurationCodePipelineActionRoleDefaultPolicyCD0DC6AA', + Roles: [ + { + Ref: 'TesterPipelineSourceConfigurationCodePipelineActionRole6DD3F86D', + }, + ], + }, + }, + }, + }); + }); + + /** + * CodePipeline tester CodeBuild project resource configuration test + */ + test(`${testNamePrefix} CodePipeline tester CodeBuild project resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TesterPipelineTesterProject3BEC9F5A: { + Type: 'AWS::CodeBuild::Project', + Properties: { + Artifacts: { + Type: 'CODEPIPELINE', + }, + Cache: { + Modes: ['LOCAL_SOURCE_CACHE'], + Type: 'LOCAL', + }, + EncryptionKey: { + Ref: 'SsmParameterValueacceleratorawsacceleratorinstallerkmskeyarnC96584B6F00A464EAD1953AFF4B05118Parameter', + }, + Environment: { + ComputeType: 'BUILD_GENERAL1_MEDIUM', + EnvironmentVariables: [ + { + Name: 'NODE_OPTIONS', + Type: 'PLAINTEXT', + Value: '--max_old_space_size=4096', + }, + { + Name: 'ACCELERATOR_REPOSITORY_NAME', + Type: 'PLAINTEXT', + Value: 'accelerator-source', + }, + { + Name: 'ACCELERATOR_REPOSITORY_BRANCH_NAME', + Type: 'PLAINTEXT', + Value: 'main', + }, + ], + Image: 'aws/codebuild/standard:5.0', + ImagePullCredentialsType: 'CODEBUILD', + PrivilegedMode: true, + Type: 'LINUX_CONTAINER', + }, + Name: 'aws-accelerator-tester-project', + ServiceRole: { + 'Fn::GetAtt': ['TesterPipelineDeployAdminRole3DA8CFF7', 'Arn'], + }, + Source: { + BuildSpec: { + 'Fn::Join': [ + '', + [ + 'version: "0.2"\nphases:\n install:\n runtime-versions:\n nodejs: 14\n build:\n commands:\n - cd source\n - yarn install\n - yarn lerna link\n - yarn build\n - cd packages/@aws-accelerator/tester\n - env\n - if [ ! -z "$MANAGEMENT_ACCOUNT_ID" ] && [ ! -z "$MANAGEMENT_ACCOUNT_ROLE_NAME" ]; then yarn run cdk deploy --require-approval never --context account=', + { + Ref: 'AWS::AccountId', + }, + ' --context region=', + { + Ref: 'AWS::Region', + }, + ' --context management-cross-account-role-name=AWSControlTowerExecution --context qualifier=aws-accelerator --context config-dir=$CODEBUILD_SRC_DIR_Config --context management-account-id=undefined --context management-account-role-name=AcceleratorAccountAccessRole; else yarn run cdk deploy --require-approval never --context account=', + { + Ref: 'AWS::AccountId', + }, + ' --context region=', + { + Ref: 'AWS::Region', + }, + ' --context management-cross-account-role-name=AWSControlTowerExecution --context config-dir=$CODEBUILD_SRC_DIR_Config; fi\n', + ], + ], + }, + Type: 'CODEPIPELINE', + }, + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/accelerator/tsconfig.json b/source/packages/@aws-accelerator/accelerator/tsconfig.json new file mode 100644 index 000000000..067f5b62f --- /dev/null +++ b/source/packages/@aws-accelerator/accelerator/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["lib/**/*", "bin/**/*", "cdk.ts", "index.ts"], + "exclude": ["cdk.out/**/*", "test/**/*"] +} diff --git a/source/packages/@aws-accelerator/config/index.ts b/source/packages/@aws-accelerator/config/index.ts new file mode 100644 index 000000000..2571c6bf1 --- /dev/null +++ b/source/packages/@aws-accelerator/config/index.ts @@ -0,0 +1,20 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +export * from './lib/accounts-config'; +export * from './lib/common-types'; +export * from './lib/global-config'; +export * from './lib/iam-config'; +export * from './lib/network-config'; +export * from './lib/organization-config'; +export * from './lib/security-config'; diff --git a/source/packages/@aws-accelerator/config/lib/accounts-config.ts b/source/packages/@aws-accelerator/config/lib/accounts-config.ts new file mode 100644 index 000000000..c990d96c0 --- /dev/null +++ b/source/packages/@aws-accelerator/config/lib/accounts-config.ts @@ -0,0 +1,371 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { throttlingBackOff } from '@aws-accelerator/utils'; +import * as AWS from 'aws-sdk'; +import * as fs from 'fs'; +import * as yaml from 'js-yaml'; +import * as path from 'path'; +import * as t from './common-types'; + +/** + * Accounts configuration items. + */ +export class AccountsConfigTypes { + static readonly accountConfig = t.interface({ + name: t.nonEmptyString, + description: t.optional(t.nonEmptyString), + email: t.nonEmptyString, + organizationalUnit: t.optional(t.nonEmptyString), + }); + + static readonly govCloudAccountConfig = t.interface({ + name: t.nonEmptyString, + description: t.optional(t.nonEmptyString), + email: t.nonEmptyString, + organizationalUnit: t.optional(t.nonEmptyString), + enableGovCloud: t.optional(t.boolean), + }); + + static readonly accountIdConfig = t.interface({ + email: t.nonEmptyString, + accountId: t.nonEmptyString, + }); + + static readonly accountsConfig = t.interface({ + mandatoryAccounts: t.array(t.union([this.accountConfig, this.govCloudAccountConfig])), + workloadAccounts: t.array(t.union([this.accountConfig, this.govCloudAccountConfig])), + accountIds: t.optional(t.array(this.accountIdConfig)), + }); +} + +export class AccountIdConfig implements t.TypeOf { + readonly email: string = ''; + readonly accountId: string = ''; +} + +/** + * Account configuration + */ +export class AccountConfig implements t.TypeOf { + /** + * The friendly name that is assigned to the account. This name will be used when creating + * a new account. The name will be used to reference this account in other + * configuration files. + * The name should not contain any spaces. + */ + readonly name: string = ''; + /** + * The description is to used to provide more information about the account. + * This value is not used when creating accounts. + */ + readonly description: string = ''; + /** + * The email address of the owner to assign to the account. The email address + * must not already be associated with another AWS acccount. You must use a + * valid email address. + * The address must be a minimum of 6 and a maximum of 64 characters long. + * All characters must be 7-bit ASCII characters + * There must be one and only one @ symbol, which separates the local name from the domain name. + * The local name can’t contain any of the following characters: whitespace, ” ‘ ( ) < > [ ] : ; , | % & + * The local name can’t begin with a dot (.) + * The domain name can consist of only the characters [a-z],[A-Z],[0-9], hyphen (-), or dot (.) + * The domain name can’t begin or end with a hyphen (-) or dot (.) + * The domain name must contain at least one dot + */ + readonly email: string = ''; + /** + * The friendly name for the Organizational Unit that this account + * is a member of. + * This Organizational Unit must exist in the organization-config.yaml file. + */ + readonly organizationalUnit: string = ''; +} + +/** + * GovCloud Account configuration + * Used instead of the account configuration in the commercial + * partition when creating GovCloud partition linked accounts. + */ +export class GovCloudAccountConfig implements t.TypeOf { + /** + * The friendly name that is assigned to the account. This name will be used when creating + * a new account. The name will be used to reference this account in other + * configuration files. + * The name should not contain any spaces. + */ + readonly name: string = ''; + /** + * The description is to used to provide more information about the account. + * This value is not used when creating accounts. + */ + readonly description: string = ''; + /** + * The email address of the owner to assign to the account. The email address + * must not already be associated with another AWS acccount. You must use a + * valid email address. + * The address must be a minimum of 6 and a maximum of 64 characters long. + * All characters must be 7-bit ASCII characters + * There must be one and only one @ symbol, which separates the local name from the domain name. + * The local name can’t contain any of the following characters: whitespace, ” ‘ ( ) < > [ ] : ; , | % & + * The local name can’t begin with a dot (.) + * The domain name can consist of only the characters [a-z],[A-Z],[0-9], hyphen (-), or dot (.) + * The domain name can’t begin or end with a hyphen (-) or dot (.) + * The domain name must contain at least one dot + */ + readonly email: string = ''; + /** + * The friendly name for the Organizational Unit that this account + * is a member of. + * This Organizational Unit must exist in the organization-config.yaml file. + */ + readonly organizationalUnit: string = ''; + /** + * Indicates whether or not a GovCloud partition account + * should be created. + */ + readonly enableGovCloud: boolean | undefined = undefined; +} +/** + * + */ +export class AccountsConfig implements t.TypeOf { + static readonly FILENAME = 'accounts-config.yaml'; + static readonly MANAGEMENT_ACCOUNT = 'Management'; + static readonly LOG_ARCHIVE_ACCOUNT = 'LogArchive'; + static readonly AUDIT_ACCOUNT = 'Audit'; + + readonly mandatoryAccounts: AccountConfig[] | GovCloudAccountConfig[] = []; + + readonly workloadAccounts: AccountConfig[] | GovCloudAccountConfig[] = []; + + public isGovCloudAccount(account: AccountConfig | GovCloudAccountConfig) { + return AccountsConfigTypes.govCloudAccountConfig.is(account); + } + + public anyGovCloudAccounts(): boolean { + for (const account of this.workloadAccounts) { + if (this.isGovCloudAccount(account)) { + return true; + } + } + return false; + } + + public isGovCloudEnabled(account: AccountConfig | GovCloudAccountConfig) { + if (AccountsConfigTypes.govCloudAccountConfig.is(account)) { + return account.enableGovCloud; + } + return false; + } + + /** + * Optionally provide a list of AWS Account IDs to bypass the usage of the + * AWS Organizations Client lookup. This is not a readonly member since we + * will initialize it with values if it is not provided + */ + public accountIds: AccountIdConfig[] | undefined = undefined; + + /** + * + * @param props + * @param values + */ + constructor( + props: { managementAccountEmail: string; logArchiveAccountEmail: string; auditAccountEmail: string }, + values?: t.TypeOf, + ) { + if (values) { + Object.assign(this, values); + } else { + this.mandatoryAccounts = [ + { + name: AccountsConfig.MANAGEMENT_ACCOUNT, + description: 'The management (primary) account. Do not change the name field for this mandatory account.', + email: props.managementAccountEmail, + organizationalUnit: 'Root', + }, + { + name: AccountsConfig.LOG_ARCHIVE_ACCOUNT, + description: 'The log archive account. Do not change the name field for this mandatory account.', + email: props.logArchiveAccountEmail, + organizationalUnit: 'Security', + }, + { + name: AccountsConfig.AUDIT_ACCOUNT, + description: + 'The security audit account (also referred to as the audit account). Do not change the name field for this mandatory account.', + email: props.auditAccountEmail, + organizationalUnit: 'Security', + }, + ]; + } + + // + // Validation errors + // + const errors: string[] = []; + + // + // Verify mandatory account names did not change + // + for (const accountName of [ + AccountsConfig.MANAGEMENT_ACCOUNT, + AccountsConfig.AUDIT_ACCOUNT, + AccountsConfig.LOG_ARCHIVE_ACCOUNT, + ]) { + if (!this.mandatoryAccounts.find(item => item.name === accountName)) { + errors.push(`Unable to find mandatory account with name ${accountName}.`); + } + } + + // + // Verify email addresses are unique + // + const accountNames = [...this.mandatoryAccounts, ...this.workloadAccounts].map(item => item.name); + if (new Set(accountNames).size !== accountNames.length) { + errors.push(`Duplicate account names defined [${accountNames}].`); + } + + // + // Verify names are unique + // + const emails = [...this.mandatoryAccounts, ...this.workloadAccounts].map(item => item.email); + if (new Set(emails).size !== emails.length) { + errors.push(`Duplicate emails defined [${emails}].`); + } + + // + // Control Tower Account Factory does not allow spaces in account names + // + for (const account of [...this.mandatoryAccounts, ...this.workloadAccounts]) { + if (account.name.indexOf(' ') > 0) { + errors.push(`Account name (${account.name}) found with spaces. Please remove spaces and retry the pipeline.`); + } + } + + if (errors.length) { + throw new Error(`${AccountsConfig.FILENAME} has ${errors.length} issues: ${errors.join(' ')}`); + } + } + + /** + * + * @param dir + * @returns + */ + static load(dir: string): AccountsConfig { + const buffer = fs.readFileSync(path.join(dir, AccountsConfig.FILENAME), 'utf8'); + const values = t.parse(AccountsConfigTypes.accountsConfig, yaml.load(buffer)); + + const managementAccountEmail = + values.mandatoryAccounts.find(value => value.name == AccountsConfig.MANAGEMENT_ACCOUNT)?.email || + '@example.com <----- UPDATE EMAIL ADDRESS'; + const logArchiveAccountEmail = + values.mandatoryAccounts.find(value => value.name == AccountsConfig.LOG_ARCHIVE_ACCOUNT)?.email || + '@example.com <----- UPDATE EMAIL ADDRESS'; + const auditAccountEmail = + values.mandatoryAccounts.find(value => value.name == AccountsConfig.AUDIT_ACCOUNT)?.email || + '@example.com <----- UPDATE EMAIL ADDRESS'; + + return new AccountsConfig( + { + managementAccountEmail, + logArchiveAccountEmail, + auditAccountEmail, + }, + values, + ); + } + + /** + * Loads account ids by utilizing the organizations client if account ids are + * not provided in the config. + */ + public async loadAccountIds(partition: string): Promise { + if (this.accountIds === undefined) { + this.accountIds = []; + } + if (this.accountIds.length == 0) { + let organizationsClient: AWS.Organizations; + if (partition === 'aws-us-gov') { + organizationsClient = new AWS.Organizations({ region: 'us-gov-west-1' }); + } else { + organizationsClient = new AWS.Organizations({ region: 'us-east-1' }); + } + + let nextToken: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => + organizationsClient.listAccounts({ NextToken: nextToken }).promise(), + ); + page.Accounts?.forEach(item => { + if (item.Email && item.Id) { + this.accountIds?.push({ email: item.Email, accountId: item.Id }); + } + }); + nextToken = page.NextToken; + } while (nextToken); + } + } + + public getAccountId(name: string): string { + const email = this.getAccount(name).email; + const accountId = this.accountIds?.find(item => item.email === email)?.accountId; + if (accountId) { + return accountId; + } + throw new Error(`name(${name}) not found`); + } + + public getAccount(name: string): AccountConfig { + const value = [...this.mandatoryAccounts, ...this.workloadAccounts].find(value => value.name == name); + if (value) { + return value; + } + throw new Error(`Account not found for ${name}`); + } + + public containsAccount(name: string): boolean { + const value = [...this.mandatoryAccounts, ...this.workloadAccounts].find(value => value.name == name); + if (value) { + return true; + } + + return false; + } + + public getManagementAccount(): AccountConfig { + return this.getAccount(AccountsConfig.MANAGEMENT_ACCOUNT); + } + + public getLogArchiveAccount(): AccountConfig { + return this.getAccount(AccountsConfig.LOG_ARCHIVE_ACCOUNT); + } + + public getAuditAccount(): AccountConfig { + return this.getAccount(AccountsConfig.AUDIT_ACCOUNT); + } + + public getManagementAccountId(): string { + return this.getAccountId(AccountsConfig.MANAGEMENT_ACCOUNT); + } + + public getLogArchiveAccountId(): string { + return this.getAccountId(AccountsConfig.LOG_ARCHIVE_ACCOUNT); + } + + public getAuditAccountId(): string { + return this.getAccountId(AccountsConfig.AUDIT_ACCOUNT); + } +} diff --git a/source/packages/@aws-accelerator/config/lib/common-types/index.ts b/source/packages/@aws-accelerator/config/lib/common-types/index.ts new file mode 100644 index 000000000..4bd050974 --- /dev/null +++ b/source/packages/@aws-accelerator/config/lib/common-types/index.ts @@ -0,0 +1,16 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +export * from './parse'; +export * from './reporter'; +export * from './types'; diff --git a/source/packages/@aws-accelerator/config/lib/common-types/parse.ts b/source/packages/@aws-accelerator/config/lib/common-types/parse.ts new file mode 100644 index 000000000..9aaf6e49a --- /dev/null +++ b/source/packages/@aws-accelerator/config/lib/common-types/parse.ts @@ -0,0 +1,26 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as t from 'io-ts'; +import { isLeft } from 'fp-ts/lib/Either'; +import { PathReporter } from './reporter'; + +export function parse(type: t.Decoder, content: S): T { + const result = type.decode(content); + if (isLeft(result)) { + const errors = PathReporter.report(result).map(error => `* ${error}`); + const errorMessage = errors.join('\n'); + throw new Error(`Could not parse content:\n${errorMessage}`); + } + return result.right; +} diff --git a/source/packages/@aws-accelerator/config/lib/common-types/reporter.ts b/source/packages/@aws-accelerator/config/lib/common-types/reporter.ts new file mode 100644 index 000000000..5ffae9cdc --- /dev/null +++ b/source/packages/@aws-accelerator/config/lib/common-types/reporter.ts @@ -0,0 +1,61 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { Context, getFunctionName, ValidationError } from 'io-ts'; +import { Reporter } from 'io-ts/lib/Reporter'; +import { fold } from 'fp-ts/lib/Either'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function stringify(v: any): string { + if (typeof v === 'function') { + return getFunctionName(v); + } + if (typeof v === 'number' && !isFinite(v)) { + if (isNaN(v)) { + return 'NaN'; + } + return v > 0 ? 'Infinity' : '-Infinity'; + } + return JSON.stringify(v); +} + +function getContextPath(context: Context): string { + return context.map(({ key }) => key).join('/'); +} + +function getMessage(e: ValidationError): string { + return e.message !== undefined + ? `${e.message} at ${getContextPath(e.context)}` + : `Invalid value ${stringify(e.value)} supplied to ${getContextPath(e.context)}`; +} + +/** + * @since 1.0.0 + */ +export function failure(es: ValidationError[]): string[] { + return es.map(getMessage); +} + +/** + * @since 1.0.0 + */ +export function success(): string[] { + return ['No errors!']; +} + +/** + * @since 1.0.0 + */ +export const PathReporter: Reporter = { + report: fold(failure, success), +}; diff --git a/source/packages/@aws-accelerator/config/lib/common-types/types.ts b/source/packages/@aws-accelerator/config/lib/common-types/types.ts new file mode 100644 index 000000000..92bc4d9f0 --- /dev/null +++ b/source/packages/@aws-accelerator/config/lib/common-types/types.ts @@ -0,0 +1,328 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { either } from 'fp-ts/lib/Either'; +/* eslint-disable @typescript-eslint/no-explicit-any */ +import * as t from 'io-ts'; +import { IPv4CidrRange } from 'ip-num'; + +export type { Any, AnyProps, Mixed, Props, TypeC, TypeOf } from 'io-ts'; +export { + array, + Array, + ArrayType, + boolean, + BooleanType, + dictionary, + DictionaryType, + interface, + InterfaceType, + intersection, + IntersectionType, + literal, + LiteralType, + number, + NumberType, + partial, + PartialType, + record, + string, + StringType, + type, + Type, + undefined, + UndefinedType, + union, + UnionType, + unknown, + UnknownType, +} from 'io-ts'; + +export class CidrType extends t.Type { + constructor(name?: string) { + super( + name ?? 'Cidr', + (value): value is IPv4CidrRange => value instanceof IPv4CidrRange, + (str, context) => + either.chain(t.string.validate(str, context), (s: string) => { + try { + return t.success(IPv4CidrRange.fromCidr(s)); + } catch (e) { + return t.failure(s, context, `Value ${s} should be a CIDR range.`); + } + }), + c => c.toCidrString(), + ); + } +} + +export class DefaultedType extends t.Type { + constructor(readonly type: T, readonly defaultValue: T['_A'], name?: string) { + super( + name ?? `Default<${type.name}>`, + type.is, + (u, c) => (u == null ? t.success(defaultValue) : type.validate(u, c)), + type.encode, + ); + } +} + +export class OptionalType extends t.Type< + T['_A'] | undefined, + T['_O'] | undefined, + T['_I'] | undefined +> { + constructor(readonly type: T, name?: string) { + super( + name ?? `Optional<${type.name}>`, + (u): u is T['_A'] | undefined => (u == null ? true : type.is(u)), + (u, c) => (u == null ? t.success(undefined) : type.validate(u, c)), + type.encode, + ); + } +} + +export type WithSize = number | string | any[] | Map | Set; + +function getSize(sized: WithSize): number { + if (typeof sized === 'number') { + return sized; + } else if (typeof sized === 'string') { + return sized.length; + } else if (Array.isArray(sized)) { + return sized.length; + } else if (sized instanceof Set) { + return sized.size; + } else if (sized instanceof Map) { + return sized.size; + } + throw new Error(`Unsupported size value ${sized}`); +} + +export interface SizedTypeProps { + readonly min?: number; + readonly max?: number; + readonly name?: string; + readonly errorMessage?: string; +} + +export class SizedType> extends t.Type { + readonly min?: number | undefined; + readonly max?: number | undefined; + + constructor(readonly type: T, readonly props: SizedTypeProps = {}) { + super( + props.name ?? `Sized<${type.name}>`, + type.is, + (u, c) => + either.chain(type.validate(u, c), (s: A) => { + const size = getSize(s); + const minValid = !props.min || (props.min && size >= props.min); + const maxValid = !props.max || (props.max && size <= props.max); + if (minValid && maxValid) { + return t.success(s); + } else { + const errorMessage = + props.errorMessage ?? `${'Value'} should be of size [${props.min ?? '-∞'}, ${props.max ?? '∞'}]`; + return t.failure(s, c, errorMessage); + } + }), + type.encode, + ); + this.min = props.min; + this.max = props.max; + } +} +export interface EnumTypeProps { + readonly name: string; + readonly errorMessage?: string; +} + +export class EnumType extends t.Type { + readonly _tag: 'EnumType' = 'EnumType'; + + constructor(readonly values: ReadonlyArray, props: EnumTypeProps) { + super( + props.name, + (u): u is T => values.some(v => v === u), + (u, c) => + this.is(u) + ? t.success(u) + : t.failure(u, c, props.errorMessage ?? `Value should be one of "${values.join('", "')}"`), + t.identity, + ); + } +} + +export type Definition

= t.TypeC

& { definitionName: string }; + +export function definition

(name: string, props: P): Definition

{ + return Object.assign(t.type(props, name), { definitionName: name }); +} + +export function isDefinition

(type: t.TypeC

): type is Definition

{ + return 'definitionName' in type; +} + +export function defaulted(type: T, defaultValue: T['_A'], name?: string): DefaultedType { + return new DefaultedType(type, defaultValue, name); +} + +export function sized = t.Type>( + type: T, + props: SizedTypeProps = {}, +): SizedType { + return new SizedType(type, props); +} + +/** + * Create an enumeration type. + */ +export function enums( + name: string, + values: ReadonlyArray, + errorMessage?: string, +): EnumType { + return new EnumType(values, { name, errorMessage }); +} + +export function optional(wrapped: T, name?: string): OptionalType { + return new OptionalType(wrapped, name); +} + +/** + * nonEmptyString comment + */ +export const nonEmptyString = sized(t.string, { + min: 1, + errorMessage: 'Value can not be empty.', +}); + +export const cidr = new CidrType(); +export type Cidr = t.TypeOf; + +export const region = enums( + 'Region', + [ + 'af-south-1', + 'ap-east-1', + 'ap-northeast-1', + 'ap-northeast-2', + 'ap-northeast-3', + 'ap-south-1', + 'ap-southeast-1', + 'ap-southeast-2', + 'ca-central-1', + 'cn-north-1', + 'cn-northwest-1', + 'eu-central-1', + 'eu-north-1', + 'eu-south-1', + 'eu-west-1', + 'eu-west-2', + 'eu-west-3', + 'me-south-1', + 'sa-east-1', + 'us-east-1', + 'us-east-2', + 'us-gov-east-1', + 'us-gov-west-1', + 'us-west-1', + 'us-west-2', + 'us-iso-west-1', + 'us-iso-east-1', + 'us-isob-east-1', + ], + 'Value should be an AWS region.', +); +export type Region = t.TypeOf; + +export const deploymentTargets = t.interface({ + organizationalUnits: optional(t.array(nonEmptyString)), + accounts: optional(t.array(nonEmptyString)), + excludedRegions: optional(t.array(nonEmptyString)), + excludedAccounts: optional(t.array(nonEmptyString)), +}); +export class DeploymentTargets implements t.TypeOf { + readonly organizationalUnits: string[] = []; + readonly accounts: string[] = []; + readonly excludedRegions: Region[] = []; + readonly excludedAccounts: string[] = []; +} + +export const storageClass = enums('storageClass', [ + 'DEEP_ARCHIVE', + 'GLACIER', + 'GLACIER_INSTANT_RETRIEVAL', + 'INFREQUENT_ACCESS', + 'INTELLIGENT_TIERING', + 'ONE_ZONE_INFREQUENT_ACCESS', + 'Value should be an AWS S3 Storage Class.', +]); +export type StorageClass = t.TypeOf; + +export const transition = t.interface({ + storageClass: storageClass, + transitionAfter: t.number, +}); + +export type Transition = t.TypeOf; + +export const lifecycleRule = t.interface({ + abortIncompleteMultipartUpload: optional(t.number), + enabled: optional(t.boolean), + expiration: optional(t.number), + expiredObjectDeleteMarker: optional(t.boolean), + id: optional(t.string), + noncurrentVersionExpiration: optional(t.number), + noncurrentVersionTransitions: optional(t.array(transition)), + transitions: optional(t.array(transition)), +}); + +export class LifecycleRule implements t.TypeOf { + readonly abortIncompleteMultipartUpload: number = 1; + readonly enabled: boolean = true; + readonly expiration: number = 1825; + readonly expiredObjectDeleteMarker: boolean = false; + readonly id: string = ''; + readonly noncurrentVersionExpiration: number = 366; + readonly noncurrentVersionTransitions: Transition[] = []; + readonly transitions: Transition[] = []; +} + +export const shareTargets = t.interface({ + organizationalUnits: optional(t.array(nonEmptyString)), + accounts: optional(t.array(nonEmptyString)), +}); +export class ShareTargets implements t.TypeOf { + readonly organizationalUnits: string[] = []; + readonly accounts: string[] = []; +} + +export const allowDeny = enums('AllowDeny', ['allow', 'deny'], 'Value should be allow or deny'); +export type AllowDeny = t.TypeOf; + +export const enableDisable = enums('EnableDisable', ['enable', 'disable'], 'Value should be enable or disable'); +export type EnableDisable = t.TypeOf; + +export const availabilityZone = enums('AvailabilityZone', ['a', 'b', 'c', 'd', 'e', 'f']); +export type AvailabilityZone = t.TypeOf; + +export const tag = t.interface({ + key: t.string, + value: t.string, +}); +export class Tag implements t.TypeOf { + readonly key: string = ''; + readonly value: string = ''; +} diff --git a/source/packages/@aws-accelerator/config/lib/global-config.ts b/source/packages/@aws-accelerator/config/lib/global-config.ts new file mode 100644 index 000000000..e959ea8a2 --- /dev/null +++ b/source/packages/@aws-accelerator/config/lib/global-config.ts @@ -0,0 +1,639 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as fs from 'fs'; +import * as yaml from 'js-yaml'; +import * as path from 'path'; + +import * as t from './common-types'; + +/** + * Global configuration items. + */ +export abstract class GlobalConfigTypes { + static readonly controlTowerConfig = t.interface({ + enable: t.boolean, + }); + + static readonly cloudtrailConfig = t.interface({ + enable: t.boolean, + organizationTrail: t.boolean, + }); + + static readonly sessionManagerConfig = t.interface({ + sendToCloudWatchLogs: t.boolean, + sendToS3: t.boolean, + excludeRegions: t.optional(t.array(t.region)), + excludeAccounts: t.optional(t.array(t.string)), + }); + + static readonly accessLogBucketConfig = t.interface({ + lifecycleRules: t.array(t.lifecycleRule), + }); + + static readonly centralLogBucketConfig = t.interface({ + lifecycleRules: t.array(t.lifecycleRule), + }); + + static readonly loggingConfig = t.interface({ + account: t.nonEmptyString, + cloudtrail: GlobalConfigTypes.cloudtrailConfig, + sessionManager: GlobalConfigTypes.sessionManagerConfig, + accessLogBucket: t.optional(GlobalConfigTypes.accessLogBucketConfig), + centralLogBucket: t.optional(GlobalConfigTypes.centralLogBucketConfig), + }); + + static readonly artifactTypeEnum = t.enums('ArtifactType', ['REDSHIFT', 'QUICKSIGHT', 'ATHENA']); + + static readonly costAndUsageReportConfig = t.interface({ + additionalSchemaElements: t.optional(t.array(t.nonEmptyString)), + compression: t.enums('CompressionType', ['ZIP', 'GZIP', 'Parquet']), + format: t.enums('FormatType', ['textORcsv', 'Parquet']), + reportName: t.nonEmptyString, + s3Prefix: t.nonEmptyString, + timeUnit: t.enums('TimeCoverageType', ['HOURLY', 'DAILY', 'MONTHLY']), + additionalArtifacts: t.optional(t.array(this.artifactTypeEnum)), + refreshClosedReports: t.boolean, + reportVersioning: t.enums('VersioningType', ['CREATE_NEW_REPORT', 'OVERWRITE_REPORT']), + lifecycleRules: t.optional(t.array(t.lifecycleRule)), + }); + + static readonly notificationConfig = t.interface({ + type: t.enums('NotificationType', ['ACTUAL', 'FORECASTED']), + thresholdType: t.enums('ThresholdType', ['PERCENTAGE', 'ABSOLUTE_VALUE']), + comparisonOperator: t.enums('ComparisonType', ['GREATER_THAN', 'LESS_THAN', 'EQUAL_TO']), + threshold: t.optional(t.number), + address: t.optional(t.nonEmptyString), + subscriptionType: t.enums('SubscriptionType', ['EMAIL', 'SNS']), + }); + + static readonly budgetConfig = t.interface({ + amount: t.number, + name: t.nonEmptyString, + type: t.enums('NotificationType', [ + 'USAGE', + 'COST', + 'RI_UTILIZATION', + 'RI_COVERAGE', + 'SAVINGS_PLANS_UTILIZATION', + 'SAVINGS_PLANS_COVERAGE', + ]), + timeUnit: t.enums('TimeUnitType', ['DAILY', 'MONTHLY', 'QUARTERLY', 'ANNUALLY']), + includeUpfront: t.optional(t.boolean), + includeTax: t.optional(t.boolean), + includeSupport: t.optional(t.boolean), + includeSubscription: t.optional(t.boolean), + includeRecurring: t.optional(t.boolean), + includeOtherSubscription: t.optional(t.boolean), + includeCredit: t.optional(t.boolean), + includeDiscount: t.optional(t.boolean), + includeRefund: t.optional(t.boolean), + useAmortized: t.optional(t.boolean), + useBlended: t.optional(t.boolean), + unit: t.optional(t.nonEmptyString), + notifications: t.optional(t.array(this.notificationConfig)), + }); + + static readonly reportConfig = t.interface({ + costAndUsageReport: t.optional(this.costAndUsageReportConfig), + budgets: t.optional(t.array(this.budgetConfig)), + }); + + static readonly globalConfig = t.interface({ + homeRegion: t.nonEmptyString, + enabledRegions: t.array(t.region), + managementAccountAccessRole: t.nonEmptyString, + cloudwatchLogRetentionInDays: t.number, + controlTower: GlobalConfigTypes.controlTowerConfig, + logging: GlobalConfigTypes.loggingConfig, + reports: t.optional(GlobalConfigTypes.reportConfig), + }); +} + +/** + * AWS ControlTower configuration + */ +export class ControlTowerConfig implements t.TypeOf { + /** + * Indicates whether AWS ControlTower enabled. + * + * When control tower is enabled, accelerator makes sure account configuration file have three mandatory AWS CT accounts. + * In AWS Control Tower, three shared accounts in your landing zone are provisioned automatically during setup: the management account, + * the log archive account, and the audit account. + */ + readonly enable = true; +} + +/** + * AWS Cloudtrail configuration + */ +export class CloudtrailConfig implements t.TypeOf { + /** + * Indicates whether AWS Cloudtrail enabled. + * + * Cloudtrail a service that helps you enable governance, compliance, and operational and risk auditing of your AWS account. + */ + readonly enable = false; + /** + * Indicates whether AWS OrganizationTrail enabled. + * + * When OrganizationTrail and cloudtrail is enabled accelerator will enable trusted access designates CloudTrail as a trusted service in your organization. + * A trusted service can query the organization's structure and create service-linked roles in the organization's accounts. + */ + readonly organizationTrail = false; +} + +/** + * AWS SessionManager configuration + */ +export class SessionManagerConfig implements t.TypeOf { + /** + * Indicates whether sending SessionManager logs to CloudWatchLogs enabled. + */ + readonly sendToCloudWatchLogs = false; + /** + * Indicates whether sending SessionManager logs to S3 enabled. + * + * When this flag is on, accelerator will send session manager logs to Central log bucket in LogArchive account. + */ + readonly sendToS3 = false; + /** + * List of AWS Region names to be excluded from configuring SessionManager configuration + */ + readonly excludeRegions = []; + /** + * List of AWS Account names to be excluded from configuring SessionManager configuration + */ + readonly excludeAccounts = []; +} + +/** + * Accelerator global logging configuration + */ +export class AccessLogBucketConfig implements t.TypeOf { + /** + * Declaration of (S3 Bucket) Lifecycle rules. + */ + readonly lifecycleRules: t.LifecycleRule[] = []; +} +export class CentralLogBucketConfig implements t.TypeOf { + /** + * Declaration of (S3 Bucket) Lifecycle rules. + */ + readonly lifecycleRules: t.LifecycleRule[] = []; +} +export class LoggingConfig implements t.TypeOf { + /** + * Accelerator logging account name. + * Accelerator use LogArchive account for global logging. + * This account maintains consolidated logs. + */ + readonly account = 'LogArchive'; + /** + * CloudTrail logging configuration + */ + readonly cloudtrail: CloudtrailConfig = new CloudtrailConfig(); + /** + * SessionManager logging configuration + */ + readonly sessionManager: SessionManagerConfig = new SessionManagerConfig(); + /** + * Declaration of a (S3 Bucket) Lifecycle rule configuration. + */ + readonly accessLogBucket: AccessLogBucketConfig | undefined = undefined; + /** + * Declaration of a (S3 Bucket) Lifecycle rule configuration. + */ + readonly centralLogBucket: CentralLogBucketConfig | undefined = undefined; +} + +/** + * CostAndUsageReport configuration + */ +export class CostAndUsageReportConfig implements t.TypeOf { + /** + * A list of strings that indicate additional content that Amazon Web Services includes in the report, such as individual resource IDs. + */ + readonly additionalSchemaElements = ['']; + /** + * The compression format that Amazon Web Services uses for the report. + */ + readonly compression = ''; + /** + * The format that Amazon Web Services saves the report in. + */ + readonly format = ''; + /** + * The name of the report that you want to create. The name must be unique, is case sensitive, and can't include spaces. + */ + readonly reportName = ''; + /** + * The prefix that Amazon Web Services adds to the report name when Amazon Web Services delivers the report. Your prefix can't include spaces. + */ + readonly s3Prefix = ''; + /** + * The granularity of the line items in the report. + */ + readonly timeUnit = ''; + /** + * A list of manifests that you want Amazon Web Services to create for this report. + */ + readonly additionalArtifacts = undefined; + /** + * Whether you want Amazon Web Services to update your reports after they have been finalized if Amazon Web Services detects charges related to previous months. These charges can include refunds, credits, or support fees. + */ + readonly refreshClosedReports = true; + /** + * Whether you want Amazon Web Services to overwrite the previous version of each report or to deliver the report in addition to the previous versions. + */ + readonly reportVersioning = ''; + /** + * Declaration of (S3 Bucket) Lifecycle rules. + */ + readonly lifecycleRules: t.LifecycleRule[] | undefined = undefined; +} + +/** + * BudgetReport configuration + */ +export class BudgetReportConfig implements t.TypeOf { + /** + * The cost or usage amount that's associated with a budget forecast, actual spend, or budget threshold. + * + * @default 2000 + */ + readonly amount = 2000; + /** + * The name of a budget. The value must be unique within an account. BudgetName can't include : and \ characters. If you don't include value for BudgetName in the template, Billing and Cost Management assigns your budget a randomly generated name. + */ + readonly name = ''; + /** + * The length of time until a budget resets the actual and forecasted spend. DAILY is available only for RI_UTILIZATION and RI_COVERAGE budgets. + */ + readonly timeUnit = ''; + /** + * Specifies whether this budget tracks costs, usage, RI utilization, RI coverage, Savings Plans utilization, or Savings Plans coverage. + */ + readonly type = ''; + /** + * Specifies whether a budget includes upfront RI costs. + * + * @default true + */ + readonly includeUpfront = true; + /** + * Specifies whether a budget includes taxes. + * + * @default true + */ + readonly includeTax = true; + /** + * Specifies whether a budget includes support subscription fees. + * + * @default true + */ + readonly includeSupport = true; + /** + * Specifies whether a budget includes non-RI subscription costs. + * + * @default true + */ + readonly includeOtherSubscription = true; + /** + * Specifies whether a budget includes subscriptions. + * + * @default true + */ + readonly includeSubscription = true; + /** + * Specifies whether a budget includes recurring fees such as monthly RI fees. + * + * @default true + */ + readonly includeRecurring = true; + /** + * Specifies whether a budget includes discounts. + * + * @default true + */ + readonly includeDiscount = true; + /** + * Specifies whether a budget includes refunds. + * + * @default true + */ + readonly includeRefund = false; + /** + * Specifies whether a budget includes credits. + * + * @default true + */ + readonly includeCredit = false; + /** + * Specifies whether a budget uses the amortized rate. + * + * @default false + */ + readonly useAmortized = false; + /** + * Specifies whether a budget uses a blended rate. + * + * @default false + */ + readonly useBlended = false; + /** + * The type of notification that AWS sends to a subscriber. + * + * An enum value that specifies the target subscription type either EMAIL or SNS + */ + readonly subscriptionType = ''; + /** + * The unit of measurement that's used for the budget forecast, actual spend, or budget threshold, such as USD or GBP. + */ + readonly unit = ''; + /** + * The type of threshold for a notification. For ABSOLUTE_VALUE thresholds, + * AWS notifies you when you go over or are forecasted to go over your total cost threshold. + * For PERCENTAGE thresholds, AWS notifies you when you go over or are forecasted to go over a certain percentage of your forecasted spend. For example, + * if you have a budget for 200 dollars and you have a PERCENTAGE threshold of 80%, AWS notifies you when you go over 160 dollars. + */ + /** + * The comparison that's used for the notification that's associated with a budget. + */ + readonly notifications = [ + { + type: '', + thresholdType: '', + comparisonOperator: '', + threshold: 90, + address: '', + subscriptionType: '', + }, + ]; +} + +/** + * Accelerator report configuration + */ +export class ReportConfig implements t.TypeOf { + /** + * Cost and usage report configuration + * + * If you want to create cost and usage report with daily granularity of the line items in the report, you need to provide below value for this parameter. + * + * @example + * ``` + * costAndUsageReport: + * compression: Parquet + * format: Parquet + * reportName: accelerator-cur + * s3Prefix: cur + * timeUnit: DAILY + * refreshClosedReports: true + * reportVersioning: CREATE_NEW_REPORT + * lifecycleRules: + * storageClass: DEEP_ARCHIVE + * enabled: true + * multiPart: 1 + * expiration: 1825 + * deleteMarker: false + * nonCurrentExpiration: 366 + * transitionAfter: 365 + * ``` + */ + readonly costAndUsageReport = new CostAndUsageReportConfig(); + /** + * Budget report configuration + * + * If you want to create budget report with monthly granularity of the line items in the report and other default parameters , you need to provide below value for this parameter. + * + * @example + * ``` + * budgets: + * - name: accel-budget + * timeUnit: MONTHLY + * type: COST + * amount: 2000 + * includeUpfront: true + * includeTax: true + * includeSupport: true + * includeSubscription: true + * includeRecurring: true + * includeOtherSubscription: true + * includeDiscount: true + * includeCredit: false + * includeRefund: false + * useBlended: false + * useAmortized: false + * unit: USD + * notification: + * - type: ACTUAL + * thresholdType: PERCENTAGE + * threshold: 90 + * comparisonOperator: GREATER_THAN + * subscriptionType: EMAIL + * address: myemail+pa-budg@example.com + * ``` + */ + readonly budgets: BudgetReportConfig[] = []; +} + +/** + * Accelerator global configuration + */ +export class GlobalConfig implements t.TypeOf { + /** + * Global configuration file name, this file must be present in accelerator config repository + */ + static readonly FILENAME = 'global-config.yaml'; + + /** + * Accelerator home region name. The region where accelerator pipeline deployed. + * + * To use us-east-1 as home region for the accelerator, you need to provide below value for this parameter. + * Note: Variable HOME_REGION created for future usage of home region in the file + * + * @example + * ``` + * homeRegion: &HOME_REGION us-east-1 + * ``` + */ + readonly homeRegion: string = ''; + /** + * List of AWS Region names where accelerator will be deployed. Home region must be part of this list. + * + * To add us-west-2 along with home region for accelerator deployment, you need to provide below value for this parameter. + * + * @example + * ``` + * enabledRegions: + * - *HOME_REGION + * - us-west-2 + * ``` + */ + readonly enabledRegions: t.Region[] = []; + + /** + * This role trusts the management account, allowing users in the management + * account to assume the role, as permitted by the management account + * administrator. The role has administrator permissions in the new member + * account. + * + * Examples: + * - AWSControlTowerExecution + * - OrganizationAccountAccessRole + */ + readonly managementAccountAccessRole = 'AWSControlTowerExecution'; + + /** + * CloudWatchLogs retention in days, accelerator's custom resource lambda function logs retention period is configured based on this value. + */ + readonly cloudwatchLogRetentionInDays = 3653; + + /** + * AWS ControlTower configuration + * + * To indicate environment has control tower enabled, you need to provide below value for this parameter. + * + * @example + * ``` + * controlTower: + * enable: true + * ``` + */ + readonly controlTower: ControlTowerConfig = new ControlTowerConfig(); + /** + * Accelerator logging configuration + * + * To enable organization trail and session manager logs sending to S3, you need to provide below value for this parameter. + * + * @example + * ``` + * logging: + * account: LogArchive + * cloudtrail: + * enable: false + * organizationTrail: false + * sessionManager: + * sendToCloudWatchLogs: false + * sendToS3: true + * ``` + */ + readonly logging: LoggingConfig = new LoggingConfig(); + + /** + * Report configuration + * + * To enable budget report along with cost and usage report, you need to provide below value for this parameter. + * + * @example + * ``` + * reports: + * costAndUsageReport: + * compression: Parquet + * format: Parquet + * reportName: accelerator-cur + * s3Prefix: cur + * timeUnit: DAILY + * refreshClosedReports: true + * reportVersioning: CREATE_NEW_REPORT + * budgets: + * - name: accel-budget + * timeUnit: MONTHLY + * type: COST + * amount: 2000 + * includeUpfront: true + * includeTax: true + * includeSupport: true + * includeSubscription: true + * includeRecurring: true + * includeOtherSubscription: true + * includeDiscount: true + * includeCredit: false + * includeRefund: false + * useBlended: false + * useAmortized: false + * unit: USD + * notification: + * - type: ACTUAL + * thresholdType: PERCENTAGE + * threshold: 90 + * comparisonOperator: GREATER_THAN + * subscriptionType: EMAIL + * address: myemail+pa-budg@example.com + * ``` + */ + readonly reports: ReportConfig | undefined = undefined; + + /** + * + * @param props + * @param values + */ + constructor( + props: { + homeRegion: string; + }, + values?: t.TypeOf, + ) { + if (values) { + Object.assign(this, values); + } else { + this.homeRegion = props.homeRegion; + this.enabledRegions = [props.homeRegion as t.Region]; + } + + // + // Validation errors + // + const errors: string[] = []; + + if (errors.length) { + throw new Error(`${GlobalConfig.FILENAME} has ${errors.length} issues: ${errors.join(' ')}`); + } + } + + /** + * Load from file in given directory + * @param dir + * @returns + */ + static load(dir: string): GlobalConfig { + const buffer = fs.readFileSync(path.join(dir, GlobalConfig.FILENAME), 'utf8'); + const values = t.parse(GlobalConfigTypes.globalConfig, yaml.load(buffer)); + + const homeRegion = values.homeRegion; + + return new GlobalConfig( + { + homeRegion, + }, + values, + ); + } + + /** + * Load from string content + * @param content + */ + static loadFromString(content: string): GlobalConfig | undefined { + try { + const values = t.parse(GlobalConfigTypes.globalConfig, yaml.load(content)); + return new GlobalConfig(values); + } catch (e) { + console.log('[global-config] Error parsing input, global config undefined'); + console.log(`${e}`); + return undefined; + } + } +} diff --git a/source/packages/@aws-accelerator/config/lib/iam-config.ts b/source/packages/@aws-accelerator/config/lib/iam-config.ts new file mode 100644 index 000000000..20d4093d1 --- /dev/null +++ b/source/packages/@aws-accelerator/config/lib/iam-config.ts @@ -0,0 +1,522 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as t from './common-types'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as yaml from 'js-yaml'; + +/** + * IAM Configuration items. + */ +export class IamConfigTypes { + /** + * SAML provider configuration + */ + static readonly samlProviderConfig = t.interface({ + name: t.nonEmptyString, + metadataDocument: t.nonEmptyString, + }); + + /** + * IAM user configuration + */ + static readonly userConfig = t.interface({ + username: t.nonEmptyString, + group: t.nonEmptyString, + boundaryPolicy: t.optional(t.nonEmptyString), + }); + + /** + * User set configuration + */ + static readonly userSetConfig = t.interface({ + deploymentTargets: t.deploymentTargets, + users: t.array(this.userConfig), + }); + + /** + * IAM policies config + */ + static readonly policiesConfig = t.interface({ + awsManaged: t.optional(t.array(t.nonEmptyString)), + customerManaged: t.optional(t.array(t.nonEmptyString)), + }); + + /** + * IAM group configuration + */ + static readonly groupConfig = t.interface({ + name: t.nonEmptyString, + policies: t.optional(this.policiesConfig), + }); + + /** + * Group set configuration + */ + static readonly groupSetConfig = t.interface({ + deploymentTargets: t.deploymentTargets, + groups: t.array(this.groupConfig), + }); + + /** + * An enum for assume by configuration + * + * Possible values service, account or provider + */ + static readonly assumedByTypeEnum = t.enums('AssumedByConfigType', ['service', 'account', 'provider']); + + /** + * Assumedby configuration + */ + static readonly assumedByConfig = t.interface({ + /** + * Type of IAM principal like service, account or provider, which can assume this role. + */ + type: this.assumedByTypeEnum, + /** + * IAM principal of either service, account or provider type. + * + * IAM principal of sns service type (i.e. new ServicePrincipal('sns.amazonaws.com')), which can assume this role. + */ + principal: t.optional(t.nonEmptyString), + }); + + /** + * IAM role configuration + */ + static readonly roleConfig = t.interface({ + /** + * A name for the IAM role. For valid values, see the RoleName parameter for the CreateRole action in the IAM API Reference. + * + */ + name: t.nonEmptyString, + /** + * Indicates whether role is used for EC2 instance profile + */ + instanceProfile: t.optional(t.boolean), + /** + * AssumedBy configuration + */ + assumedBy: t.array(this.assumedByConfig), + /** + * Policies configuration + */ + policies: t.optional(this.policiesConfig), + /** + * A permissions boundary configuration + */ + boundaryPolicy: t.optional(t.nonEmptyString), + }); + + /** + * IAM role set configuration + */ + static readonly roleSetConfig = t.interface({ + /** + * Role set deployment targets + */ + deploymentTargets: t.deploymentTargets, + /** + * List of role objects + */ + roles: t.array(this.roleConfig), + }); + + /** + * IAM policy configuration + */ + static readonly policyConfig = t.interface({ + name: t.nonEmptyString, + policy: t.nonEmptyString, + }); + + /** + * IAM policy set configuration + */ + static readonly policySetConfig = t.interface({ + deploymentTargets: t.deploymentTargets, + policies: t.array(this.policyConfig), + }); + + /** + * IAM configuration + */ + static readonly iamConfig = t.interface({ + providers: t.optional(t.array(this.samlProviderConfig)), + policySets: t.optional(t.array(this.policySetConfig || [])), + roleSets: t.optional(t.array(this.roleSetConfig)), + groupSets: t.optional(t.array(this.groupSetConfig)), + userSets: t.optional(t.array(this.userSetConfig)), + }); +} + +/** + * SAML provider configuration + */ +export class SamlProviderConfig implements t.TypeOf { + /** + * The name of the provider to create. + * + * This parameter allows a string of characters consisting of upper and lowercase alphanumeric characters with no spaces. You can also include any of the following characters: _+=,.@- + * + * Length must be between 1 and 128 characters. + * + * @default a CloudFormation generated name + */ + readonly name: string = ''; + /** + * SAML metadata document XML file, this file must be present in config repository + */ + readonly metadataDocument: string = ''; +} + +/** + * IAM User configuration + */ +export class UserConfig implements t.TypeOf { + /** + * A name for the IAM user. For valid values, see the UserName parameter for the CreateUser action in the IAM API Reference. + * If you don't specify a name, AWS CloudFormation generates a unique physical ID and uses that ID for the user name. + * + * If you specify a name, you cannot perform updates that require replacement of this resource. + * You can perform updates that require no or some interruption. If you must replace the resource, specify a new name. + */ + readonly username: string = ''; + /** + * AWS supports permissions boundaries for IAM entities (users or roles). + * A permissions boundary is an advanced feature for using a managed policy to set the maximum permissions that an identity-based policy can grant to an IAM entity. + * An entity's permissions boundary allows it to perform only the actions that are allowed by both its identity-based policies and its permissions boundaries. + * + * Permission boundary is derived from iam-policies/boundary-policy.json file in config repository + */ + readonly boundaryPolicy: string = ''; + /** + * Group to add this user to. + */ + readonly group: string = ''; +} + +/** + * User set configuration + */ +export class UserSetConfig implements t.TypeOf { + /** + * User set's deployment target + */ + readonly deploymentTargets: t.DeploymentTargets = new t.DeploymentTargets(); + /** + * List os user objects + */ + readonly users: UserConfig[] = []; +} + +/** + * IAM policies configuration + */ +export class PoliciesConfig implements t.TypeOf { + /** + * List of AWS managed policies + */ + readonly awsManaged: string[] = []; + /** + * List of Customer managed policies + */ + readonly customerManaged: string[] = []; +} + +/** + * IAM group configuration + */ +export class GroupConfig implements t.TypeOf { + /** + * A name for the IAM group. For valid values, see the GroupName parameter for the CreateGroup action in the IAM API Reference. + * If you don't specify a name, AWS CloudFormation generates a unique physical ID and uses that ID for the group name. + * + * If you specify a name, you must specify the CAPABILITY_NAMED_IAM value to acknowledge your template's capabilities. + * For more information, see Acknowledging IAM Resources in AWS CloudFormation Templates. + */ + readonly name: string = ''; + /** + * List of policy objects + */ + readonly policies: PoliciesConfig | undefined = undefined; +} + +/** + * IAM group set configuration + */ +export class GroupSetConfig implements t.TypeOf { + /** + * Group set's deployment targets + */ + readonly deploymentTargets: t.DeploymentTargets = new t.DeploymentTargets(); + /** + * List of IAM group objects + */ + readonly groups: GroupConfig[] = []; +} + +/** + * Assumedby configuration + */ +export class AssumedByConfig implements t.TypeOf { + /** + * IAM principal of either service, account or provider type. + * + * IAM principal of sns service type (i.e. new ServicePrincipal('sns.amazonaws.com')), which can assume this role. + */ + readonly principal: string = ''; + /** + * Type of IAM principal type like service, account or provider, which can assume this role. + */ + readonly type!: t.TypeOf; +} + +/** + * IAM Role configuration + */ +export class RoleConfig implements t.TypeOf { + /** + * AssumedBy configuration + */ + readonly assumedBy: AssumedByConfig[] = []; + /** + * Indicates whether role is used for EC2 instance profile + */ + readonly instanceProfile: boolean | undefined = undefined; + /** + * A permissions boundary configuration + */ + readonly boundaryPolicy: string = ''; + /** + * A name for the role + */ + readonly name: string = ''; + /** + * List of policies for the role + */ + readonly policies: PoliciesConfig | undefined = undefined; +} + +/** + * Role set configuration + */ +export class RoleSetConfig implements t.TypeOf { + /** + * Role set deployment targets + */ + readonly deploymentTargets: t.DeploymentTargets = new t.DeploymentTargets(); + /** + * List of role objects + */ + readonly roles: RoleConfig[] = []; +} + +/** + * IAM policy configuration + */ +export class PolicyConfig implements t.TypeOf { + /** + * A name for the policy + */ + readonly name: string = ''; + /** + * A XML file containing policy boundary definition + */ + readonly policy: string = ''; +} + +/** + * Policy set configuration + */ +export class PolicySetConfig implements t.TypeOf { + /** + * Policy set deployment targets + */ + readonly deploymentTargets: t.DeploymentTargets = new t.DeploymentTargets(); + readonly policies: PolicyConfig[] = []; +} + +/** + * IAM configuration + */ +export class IamConfig implements t.TypeOf { + /** + * A name for the iam config file in config repository + * + * @default iam-config.yaml + */ + static readonly FILENAME = 'iam-config.yaml'; + + /** + * SAML provider configuration + * To configure SAML configuration, you need to provide the following values for this parameter. + * Replace provider name and metadata document file. Document file must be in config repository + * + * @example + * ``` + * providers: + * name: , + * metadataDocument: , + */ + readonly providers: SamlProviderConfig[] = []; + + /** + * Policy set configuration. + * + * To configure IAM policy named Default-Boundary-Policy with permission boundary defined in iam-policies/boundary-policy.json file, you need to provide following values for this parameter. + * + * @example + *``` + * policySets: + * - deploymentTargets: + * organizationalUnits: + * - Root + * policies: + * - name: Default-Boundary-Policy + * policy: iam-policies/boundary-policy.json + * ``` + */ + readonly policySets: PolicySetConfig[] = []; + + /** + * Role sets configuration + * + * To configure EC2-Default-SSM-AD-Role role to be assumed by ec2 service into Root and Infrastructure organizational units, + * you need to provide following values for this parameter. This role will have AmazonSSMManagedInstanceCore, AmazonSSMDirectoryServiceAccess and CloudWatchAgentServerPolicy policy + * with permission boundary defined by Default-Boundary-Policy + * + * @example + * ``` + * roleSets: + * - deploymentTargets: + * organizationalUnits: + * - Root + * roles: + * - name: EC2-Default-SSM-AD-Role + * assumedBy: + * - type: service + * principal: ec2.amazonaws.com + * policies: + * awsManaged: + * - AmazonSSMManagedInstanceCore + * - AmazonSSMDirectoryServiceAccess + * - CloudWatchAgentServerPolicy + * boundaryPolicy: Default-Boundary-Policy + * ``` + */ + readonly roleSets: RoleSetConfig[] = []; + + /** + * Group set configuration + * + * To configure IAM group named Administrators into Root and Infrastructure organizational units, you need to provide following values for this parameter. + * + * @example + * ``` + * groupSets: + * - deploymentTargets: + * organizationalUnits: + * - Root + * groups: + * - name: Administrators + * policies: + * awsManaged: + * - AdministratorAccess + * ``` + */ + readonly groupSets: GroupSetConfig[] = []; + + /** + * User set configuration + * + * To configure breakGlassUser01 user into Administrators in Management account, you need to provide following values for this parameter. + * + * @example + * ``` + * userSets: + * - deploymentTargets: + * accounts: + * - Management + * users: + * - username: breakGlassUser01 + * group: Administrators + * boundaryPolicy: Default-Boundary-Policy + * ``` + * + */ + readonly userSets: UserSetConfig[] = []; + + /** + * + * @param values + * @param configDir + */ + constructor(values?: t.TypeOf, configDir?: string) { + // + // Validation errors + // + const errors: string[] = []; + + if (values) { + const policies: { name: string; policyFile: string }[] = []; + for (const policySet of values.policySets ?? []) { + for (const policy of policySet.policies) { + policies.push({ name: policy.name, policyFile: policy.policy }); + } + } + + // Validate policy file existence + if (configDir) { + for (const policy of policies) { + if (!fs.existsSync(path.join(configDir, policy.policyFile))) { + errors.push(`Policy definition file ${policy.policyFile} not found, for ${policy.name} !!!`); + } + } + } + + if (errors.length) { + throw new Error(`${IamConfig.FILENAME} has ${errors.length} issues: ${errors.join(' ')}`); + } + + Object.assign(this, values); + } + } + + /** + * Load from config file content + * @param dir + * @returns + */ + static load(dir: string): IamConfig { + const buffer = fs.readFileSync(path.join(dir, IamConfig.FILENAME), 'utf8'); + const values = t.parse(IamConfigTypes.iamConfig, yaml.load(buffer)); + return new IamConfig(values, dir); + } + + /** + * Load from string content + * @param content + */ + static loadFromString(content: string): IamConfig | undefined { + try { + const values = t.parse(IamConfigTypes.iamConfig, yaml.load(content)); + return new IamConfig(values); + } catch (e) { + console.log('[iam-config] Error parsing input, global config undefined'); + console.log(`${e}`); + return undefined; + } + } +} diff --git a/source/packages/@aws-accelerator/config/lib/network-config.ts b/source/packages/@aws-accelerator/config/lib/network-config.ts new file mode 100644 index 000000000..969e00d33 --- /dev/null +++ b/source/packages/@aws-accelerator/config/lib/network-config.ts @@ -0,0 +1,2770 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as fs from 'fs'; +import * as yaml from 'js-yaml'; +import * as path from 'path'; + +import * as t from './common-types'; + +/** + * Network configuration items. + */ +export class NetworkConfigTypes { + static readonly defaultVpcsConfig = t.interface({ + delete: t.boolean, + excludeAccounts: t.optional(t.array(t.string)), + }); + + static readonly transitGatewayRouteTableVpcEntryConfig = t.interface({ + account: t.nonEmptyString, + vpcName: t.nonEmptyString, + }); + + static readonly transitGatewayRouteEntryConfig = t.interface({ + destinationCidrBlock: t.nonEmptyString, + blackhole: t.optional(t.boolean), + attachment: t.optional(this.transitGatewayRouteTableVpcEntryConfig), + }); + + static readonly transitGatewayRouteTableConfig = t.interface({ + name: t.nonEmptyString, + tags: t.optional(t.array(t.tag)), + routes: t.array(this.transitGatewayRouteEntryConfig), + }); + + static readonly transitGatewayConfig = t.interface({ + name: t.nonEmptyString, + account: t.nonEmptyString, + region: t.region, + shareTargets: t.optional(t.shareTargets), + asn: t.number, + dnsSupport: t.enableDisable, + vpnEcmpSupport: t.enableDisable, + defaultRouteTableAssociation: t.enableDisable, + defaultRouteTablePropagation: t.enableDisable, + autoAcceptSharingAttachments: t.enableDisable, + routeTables: t.array(this.transitGatewayRouteTableConfig), + tags: t.optional(t.array(t.tag)), + }); + + static readonly routeTableEntryTypeEnum = t.enums( + 'Type', + [ + 'transitGateway', + 'natGateway', + 'internetGateway', + 'local', + 'gatewayEndpoint', + 'networkInterface', + 'networkFirewall', + ], + 'Value should be a route table target type', + ); + + static readonly routeTableEntryConfig = t.interface({ + name: t.nonEmptyString, + destination: t.optional(t.nonEmptyString), + type: t.optional(this.routeTableEntryTypeEnum), + target: t.nonEmptyString, + targetAvailabilityZone: t.optional(t.nonEmptyString), + }); + + static readonly routeTableConfig = t.interface({ + name: t.nonEmptyString, + routes: t.optional(t.array(this.routeTableEntryConfig)), + tags: t.optional(t.array(t.tag)), + }); + + static readonly subnetConfig = t.interface({ + name: t.nonEmptyString, + availabilityZone: t.nonEmptyString, + routeTable: t.nonEmptyString, + ipv4CidrBlock: t.nonEmptyString, + mapPublicIpOnLaunch: t.optional(t.boolean), + shareTargets: t.optional(t.shareTargets), + tags: t.optional(t.array(t.tag)), + }); + + static readonly natGatewayConfig = t.interface({ + name: t.nonEmptyString, + subnet: t.nonEmptyString, + tags: t.optional(t.array(t.tag)), + }); + + static readonly transitGatewayAttachmentTargetConfig = t.interface({ + name: t.nonEmptyString, + account: t.nonEmptyString, + }); + + static readonly transitGatewayAttachmentOptionsConfig = t.interface({ + dnsSupport: t.optional(t.enableDisable), + ipv6Support: t.optional(t.enableDisable), + applianceModeSupport: t.optional(t.enableDisable), + }); + + static readonly transitGatewayAttachmentConfig = t.interface({ + name: t.nonEmptyString, + transitGateway: this.transitGatewayAttachmentTargetConfig, + subnets: t.array(t.nonEmptyString), + options: t.optional(this.transitGatewayAttachmentOptionsConfig), + routeTableAssociations: t.optional(t.array(t.nonEmptyString)), + routeTablePropagations: t.optional(t.array(t.nonEmptyString)), + tags: t.optional(t.array(t.tag)), + }); + + static readonly ipAddressFamilyEnum = t.enums( + 'IP Address Family', + ['IPv4', 'IPv6'], + 'Value should be an ip address family type', + ); + + static readonly prefixListConfig = t.interface({ + name: t.nonEmptyString, + accounts: t.array(t.nonEmptyString), + regions: t.array(t.region), + addressFamily: this.ipAddressFamilyEnum, + maxEntries: t.number, + entries: t.array(t.nonEmptyString), + tags: t.optional(t.array(t.tag)), + }); + + static readonly gatewayEndpointEnum = t.enums( + 'GatewayEndpointType', + ['s3', 'dynamodb'], + 'Value should be a gateway endpoint type', + ); + + static readonly gatewayEndpointServiceConfig = t.interface({ + service: this.gatewayEndpointEnum, + policy: t.optional(t.nonEmptyString), + }); + + static readonly gatewayEndpointConfig = t.interface({ + defaultPolicy: t.nonEmptyString, + endpoints: t.array(this.gatewayEndpointServiceConfig), + }); + + static readonly interfaceEndpointServiceConfig = t.interface({ + service: t.nonEmptyString, + policy: t.optional(t.nonEmptyString), + }); + + static readonly interfaceEndpointConfig = t.interface({ + defaultPolicy: t.nonEmptyString, + endpoints: t.array(this.interfaceEndpointServiceConfig), + subnets: t.array(t.nonEmptyString), + central: t.optional(t.boolean), + allowedCidrs: t.optional(t.array(t.nonEmptyString)), + }); + + static readonly securityGroupRuleTypeEnum = t.enums( + 'SecurityGroupRuleType', + [ + 'RDP', + 'SSH', + 'HTTP', + 'HTTPS', + 'MYSQL', + 'MYSQL/AURORA', + 'REDSHIFT', + 'POSTGRESQL', + 'ORACLE-RDS', + 'TCP', + 'UDP', + 'ICMP', + 'ALL', + ], + 'Value should be a security group rule type', + ); + + static readonly subnetSourceConfig = t.interface({ + account: t.optional(t.nonEmptyString), + vpc: t.nonEmptyString, + subnets: t.array(t.nonEmptyString), + }); + + static readonly securityGroupSourceConfig = t.interface({ + securityGroups: t.array(t.nonEmptyString), + }); + + static readonly prefixListSourceConfig = t.interface({ + prefixLists: t.array(t.nonEmptyString), + }); + + static readonly securityGroupRuleConfig = t.interface({ + description: t.nonEmptyString, + types: t.optional(t.array(this.securityGroupRuleTypeEnum)), + tcpPorts: t.optional(t.array(t.number)), + udpPorts: t.optional(t.array(t.number)), + port: t.optional(t.number), + fromPort: t.optional(t.number), + toPort: t.optional(t.number), + sources: t.array( + t.union([t.nonEmptyString, this.subnetSourceConfig, this.securityGroupSourceConfig, this.prefixListSourceConfig]), + ), + }); + + static readonly securityGroupConfig = t.interface({ + name: t.nonEmptyString, + description: t.optional(t.nonEmptyString), + inboundRules: t.optional(t.array(this.securityGroupRuleConfig)), + outboundRules: t.optional(t.array(this.securityGroupRuleConfig)), + tags: t.optional(t.array(t.tag)), + }); + + static readonly instanceTenancyTypeEnum = t.enums( + 'InstanceTenancy', + ['default', 'dedicated'], + 'Value should be an instance tenancy type', + ); + + static readonly networkAclSubnetSelection = t.interface({ + account: t.optional(t.nonEmptyString), + vpc: t.nonEmptyString, + subnet: t.nonEmptyString, + }); + + static readonly networkAclInboundRuleConfig = t.interface({ + rule: t.number, + protocol: t.number, + fromPort: t.number, + toPort: t.number, + action: t.allowDeny, + source: t.union([t.nonEmptyString, this.networkAclSubnetSelection]), + }); + + static readonly networkAclOutboundRuleConfig = t.interface({ + rule: t.number, + protocol: t.number, + fromPort: t.number, + toPort: t.number, + action: t.allowDeny, + destination: t.union([t.nonEmptyString, this.networkAclSubnetSelection]), + }); + + static readonly networkAclConfig = t.interface({ + name: t.nonEmptyString, + subnetAssociations: t.array(t.nonEmptyString), + inboundRules: t.optional(t.array(this.networkAclInboundRuleConfig)), + outboundRules: t.optional(t.array(this.networkAclOutboundRuleConfig)), + tags: t.optional(t.array(t.tag)), + }); + + static readonly netbiosNodeEnum = t.enums('NetbiosNodeTypeEnum', [1, 2, 4, 8]); + + static readonly dhcpOptsConfig = t.interface({ + name: t.nonEmptyString, + accounts: t.array(t.nonEmptyString), + regions: t.array(t.region), + domainName: t.optional(t.nonEmptyString), + domainNameServers: t.optional(t.array(t.nonEmptyString)), + netbiosNameServers: t.optional(t.array(t.nonEmptyString)), + netbiosNodeType: t.optional(this.netbiosNodeEnum), + ntpServers: t.optional(t.array(t.nonEmptyString)), + tags: t.optional(t.array(t.tag)), + }); + + static readonly mutationProtectionEnum = t.enums('MutationProtectionTypeEnum', ['ENABLED', 'DISABLED']); + + static readonly vpcDnsFirewallAssociationConfig = t.interface({ + name: t.nonEmptyString, + priority: t.number, + mutationProtection: t.optional(this.mutationProtectionEnum), + tags: t.optional(t.array(t.tag)), + }); + + static readonly endpointPolicyConfig = t.interface({ + name: t.nonEmptyString, + document: t.nonEmptyString, + }); + + static readonly vpcConfig = t.interface({ + name: t.nonEmptyString, + account: t.nonEmptyString, + region: t.region, + cidrs: t.array(t.nonEmptyString), + dhcpOptions: t.optional(t.nonEmptyString), + dnsFirewallRuleGroups: t.optional(t.array(this.vpcDnsFirewallAssociationConfig)), + enableDnsHostnames: t.optional(t.boolean), + enableDnsSupport: t.optional(t.boolean), + gatewayEndpoints: t.optional(this.gatewayEndpointConfig), + instanceTenancy: t.optional(this.instanceTenancyTypeEnum), + interfaceEndpoints: t.optional(this.interfaceEndpointConfig), + internetGateway: t.optional(t.boolean), + natGateways: t.optional(t.array(this.natGatewayConfig)), + useCentralEndpoints: t.optional(t.boolean), + securityGroups: t.optional(t.array(this.securityGroupConfig)), + prefixLists: t.optional(t.array(this.prefixListConfig)), + networkAcls: t.optional(t.array(this.networkAclConfig)), + queryLogs: t.optional(t.array(t.nonEmptyString)), + resolverRules: t.optional(t.array(t.nonEmptyString)), + routeTables: t.optional(t.array(this.routeTableConfig)), + subnets: t.optional(t.array(this.subnetConfig)), + transitGatewayAttachments: t.optional(t.array(this.transitGatewayAttachmentConfig)), + tags: t.optional(t.array(t.tag)), + }); + + static readonly trafficTypeEnum = t.enums( + 'Flow LogTrafficType', + ['ALL', 'ACCEPT', 'REJECT'], + 'Value should be a flow log traffic type', + ); + + static readonly logDestinationTypeEnum = t.enums( + 'LogDestinationTypes', + ['s3', 'cloud-watch-logs'], + 'Value should be a log destination type', + ); + + static readonly vpcFlowLogsConfig = t.interface({ + trafficType: this.trafficTypeEnum, + maxAggregationInterval: t.number, + destinations: t.array(this.logDestinationTypeEnum), + defaultFormat: t.boolean, + customFields: t.optional(t.array(t.nonEmptyString)), + }); + + static readonly ruleTypeEnum = t.enums('ResolverRuleType', ['FORWARD', 'RECURSIVE', 'SYSTEM']); + + static readonly ruleTargetIps = t.interface({ + ip: t.nonEmptyString, + port: t.optional(t.nonEmptyString), + }); + + static readonly resolverRuleConfig = t.interface({ + name: t.nonEmptyString, + domainName: t.nonEmptyString, + inboundEndpointTarget: t.optional(t.nonEmptyString), + ruleType: t.optional(this.ruleTypeEnum), + shareTargets: t.optional(t.shareTargets), + targetIps: t.optional(t.array(this.ruleTargetIps)), + tags: t.optional(t.array(t.tag)), + }); + + static readonly resolverEndpointTypeEnum = t.enums('ResolverEndpointType', ['INBOUND', 'OUTBOUND']); + + static readonly resolverEndpointConfig = t.interface({ + name: t.nonEmptyString, + type: this.resolverEndpointTypeEnum, + vpc: t.nonEmptyString, + subnets: t.array(t.nonEmptyString), + allowedCidrs: t.optional(t.array(t.nonEmptyString)), + rules: t.optional(t.array(this.resolverRuleConfig)), + tags: t.optional(t.array(t.tag)), + }); + + static readonly dnsQueryLogsConfig = t.interface({ + name: t.nonEmptyString, + destinations: t.array(this.logDestinationTypeEnum), + shareTargets: t.optional(t.shareTargets), + }); + + static readonly dnsFirewallRuleActionTypeEnum = t.enums('DnsFirewallRuleAction', ['ALLOW', 'ALERT', 'BLOCK']); + + static readonly dnsFirewallBlockResponseTypeEnum = t.enums('DnsFirewallBlockResponseType', [ + 'NODATA', + 'NXDOMAIN', + 'OVERRIDE', + ]); + + static readonly dnsFirewallManagedDomainListEnum = t.enums('DnsFirewallManagedDomainLists', [ + 'AWSManagedDomainsBotnetCommandandControl', + 'AWSManagedDomainsMalwareDomainList', + ]); + + static readonly dnsFirewallRulesConfig = t.interface({ + name: t.nonEmptyString, + action: this.dnsFirewallRuleActionTypeEnum, + priority: t.number, + blockOverrideDomain: t.optional(t.nonEmptyString), + blockOverrideTtl: t.optional(t.number), + blockResponse: t.optional(this.dnsFirewallBlockResponseTypeEnum), + customDomainList: t.optional(t.nonEmptyString), + managedDomainList: t.optional(this.dnsFirewallManagedDomainListEnum), + }); + + static readonly dnsFirewallRuleGroupConfig = t.interface({ + name: t.nonEmptyString, + regions: t.array(t.region), + rules: t.array(this.dnsFirewallRulesConfig), + shareTargets: t.optional(t.shareTargets), + tags: t.optional(t.array(t.tag)), + }); + + static readonly resolverConfig = t.interface({ + endpoints: t.optional(t.array(this.resolverEndpointConfig)), + firewallRuleGroups: t.optional(t.array(this.dnsFirewallRuleGroupConfig)), + queryLogs: t.optional(this.dnsQueryLogsConfig), + }); + + static readonly nfwRuleType = t.enums('NfwRuleType', ['STATEFUL', 'STATELESS']); + + static readonly nfwGeneratedRulesType = t.enums('NfwGeneratedRulesType', ['ALLOWLIST', 'DENYLIST']); + + static readonly nfwTargetType = t.enums('NfwTargetType', ['TLS_SNI', 'HTTP_HOST']); + + static readonly nfwStatefulRuleActionType = t.enums('NfwStatefulRuleActionType', ['ALERT', 'DROP', 'PASS']); + + static readonly nfwStatefulRuleDirectionType = t.enums('NfwStatefulRuleDirectionType', ['ANY', 'FORWARD']); + + static readonly nfwStatefulRuleProtocolType = t.enums('NfwStatefulRuleProtocolType', [ + 'DCERPC', + 'DHCP', + 'DNS', + 'FTP', + 'HTTP', + 'ICMP', + 'IKEV2', + 'IMAP', + 'IP', + 'KRB5', + 'MSN', + 'NTP', + 'SMB', + 'SMTP', + 'SSH', + 'TCP', + 'TFTP', + 'TLS', + 'UDP', + ]); + + static readonly nfwStatelessRuleActionType = t.enums('NfwStatelessRuleActionType', [ + 'aws:pass', + 'aws:drop', + 'aws:forward_to_sfe', + ]); + + static readonly nfwStatefulRuleOptionsType = t.enums('NfwStatefulRuleOptionsType', [ + 'DEFAULT_ACTION_ORDER', + 'STRICT_ORDER', + ]); + + static readonly nfwLogType = t.enums('NfwLogType', ['ALERT', 'FLOW']); + + static readonly nfwRuleSourceListConfig = t.interface({ + generatedRulesType: this.nfwGeneratedRulesType, + targets: t.array(t.nonEmptyString), + targetTypes: t.array(this.nfwTargetType), + }); + + static readonly nfwRuleSourceStatefulRuleHeaderConfig = t.interface({ + destination: t.nonEmptyString, + destinationPort: t.nonEmptyString, + direction: this.nfwStatefulRuleDirectionType, + protocol: this.nfwStatefulRuleProtocolType, + source: t.nonEmptyString, + sourcePort: t.nonEmptyString, + }); + + static readonly nfwRuleSourceStatefulRuleOptionsConfig = t.interface({ + keyword: t.nonEmptyString, + settings: t.optional(t.array(t.nonEmptyString)), + }); + + static readonly nfwRuleSourceStatefulRuleConfig = t.interface({ + action: this.nfwStatefulRuleActionType, + header: this.nfwRuleSourceStatefulRuleHeaderConfig, + ruleOptions: t.array(this.nfwRuleSourceStatefulRuleOptionsConfig), + }); + + static readonly nfwRuleSourceCustomActionDimensionConfig = t.interface({ + dimensions: t.array(t.nonEmptyString), + }); + + static readonly nfwRuleSourceCustomActionDefinitionConfig = t.interface({ + publishMetricAction: this.nfwRuleSourceCustomActionDimensionConfig, + }); + + static readonly nfwRuleSourceCustomActionConfig = t.interface({ + actionDefinition: this.nfwRuleSourceCustomActionDefinitionConfig, + actionName: t.nonEmptyString, + }); + + static readonly nfwRuleSourceStatelessPortRangeConfig = t.interface({ + fromPort: t.number, + toPort: t.number, + }); + + static readonly nfwRuleSourceStatelessTcpFlagsConfig = t.interface({ + flags: t.array(t.nonEmptyString), + masks: t.optional(t.array(t.nonEmptyString)), + }); + + static readonly nfwRuleSourceStatelessMatchAttributesConfig = t.interface({ + destinationPorts: t.array(this.nfwRuleSourceStatelessPortRangeConfig), + destinations: t.array(t.nonEmptyString), + protocols: t.array(t.number), + sourcePorts: t.array(this.nfwRuleSourceStatelessPortRangeConfig), + sources: t.array(t.nonEmptyString), + tcpFlags: t.array(this.nfwRuleSourceStatelessTcpFlagsConfig), + }); + + static readonly nfwRuleSourceStatelessRuleDefinitionConfig = t.interface({ + actions: t.array(this.nfwStatelessRuleActionType), + matchAttributes: this.nfwRuleSourceStatelessMatchAttributesConfig, + }); + + static readonly nfwRuleSourceStatelessRuleConfig = t.interface({ + priority: t.number, + ruleDefinition: this.nfwRuleSourceStatelessRuleDefinitionConfig, + }); + + static readonly nfwStatelessRulesAndCustomActionsConfig = t.interface({ + statelessRules: t.array(this.nfwRuleSourceStatelessRuleConfig), + customActions: t.optional(t.array(this.nfwRuleSourceCustomActionConfig)), + }); + + static readonly nfwRuleSourceConfig = t.interface({ + rulesSourceList: t.optional(this.nfwRuleSourceListConfig), + rulesString: t.optional(t.nonEmptyString), + statefulRules: t.optional(t.array(this.nfwRuleSourceStatefulRuleConfig)), + statelessRulesAndCustomActions: t.optional(this.nfwStatelessRulesAndCustomActionsConfig), + }); + + static readonly nfwRuleVariableDefinitionConfig = t.interface({ + name: t.nonEmptyString, + definition: t.array(t.nonEmptyString), + }); + + static readonly nfwRuleVariableConfig = t.interface({ + ipSets: this.nfwRuleVariableDefinitionConfig, + portSets: this.nfwRuleVariableDefinitionConfig, + }); + + static readonly nfwRuleGroupRuleConfig = t.interface({ + rulesSource: this.nfwRuleSourceConfig, + ruleVariables: t.optional(this.nfwRuleVariableConfig), + statefulRuleOptions: t.optional(this.nfwStatefulRuleOptionsType), + }); + + static readonly nfwRuleGroupConfig = t.interface({ + name: t.nonEmptyString, + regions: t.array(t.region), + capacity: t.number, + type: this.nfwRuleType, + description: t.optional(t.nonEmptyString), + ruleGroup: t.optional(this.nfwRuleGroupRuleConfig), + shareTargets: t.optional(t.shareTargets), + tags: t.optional(t.array(t.tag)), + }); + + static readonly nfwStatefulRuleGroupReferenceConfig = t.interface({ + name: t.nonEmptyString, + priority: t.optional(t.number), + }); + + static readonly nfwStatelessRuleGroupReferenceConfig = t.interface({ + name: t.nonEmptyString, + priority: t.number, + }); + + static readonly nfwFirewallPolicyPolicyConfig = t.interface({ + statefulDefaultActions: t.optional(t.array(t.nonEmptyString)), + statefulEngineOptions: t.optional(this.nfwStatefulRuleOptionsType), + statefulRuleGroups: t.optional(t.array(this.nfwStatefulRuleGroupReferenceConfig)), + statelessCustomActions: t.optional(t.array(this.nfwRuleSourceCustomActionConfig)), + statelessDefaultActions: t.array(t.union([this.nfwStatelessRuleActionType, t.nonEmptyString])), + statelessFragmentDefaultActions: t.array(t.union([this.nfwStatelessRuleActionType, t.nonEmptyString])), + statelessRuleGroups: t.optional(t.array(this.nfwStatelessRuleGroupReferenceConfig)), + }); + + static readonly nfwFirewallPolicyConfig = t.interface({ + name: t.nonEmptyString, + firewallPolicy: this.nfwFirewallPolicyPolicyConfig, + regions: t.array(t.region), + description: t.optional(t.nonEmptyString), + shareTargets: t.optional(t.shareTargets), + tags: t.optional(t.array(t.tag)), + }); + + static readonly nfwLoggingConfig = t.interface({ + destination: this.logDestinationTypeEnum, + type: this.nfwLogType, + }); + + static readonly nfwFirewallConfig = t.interface({ + name: t.nonEmptyString, + firewallPolicy: t.nonEmptyString, + subnets: t.array(t.nonEmptyString), + vpc: t.nonEmptyString, + deleteProtection: t.optional(t.boolean), + description: t.optional(t.nonEmptyString), + firewallPolicyChangeProtection: t.optional(t.boolean), + subnetChangeProtection: t.optional(t.boolean), + loggingConfiguration: t.optional(t.array(this.nfwLoggingConfig)), + tags: t.optional(t.array(t.tag)), + }); + + static readonly nfwConfig = t.interface({ + firewalls: t.array(this.nfwFirewallConfig), + policies: t.array(this.nfwFirewallPolicyConfig), + rules: t.array(this.nfwRuleGroupConfig), + }); + + static readonly centralNetworkServicesConfig = t.interface({ + delegatedAdminAccount: t.nonEmptyString, + route53Resolver: t.optional(this.resolverConfig), + networkFirewall: t.optional(this.nfwConfig), + }); + + static readonly vpcPeeringConfig = t.interface({ + name: t.nonEmptyString, + vpcs: t.array(t.nonEmptyString), + tags: t.optional(t.array(t.tag)), + }); + + static readonly networkConfig = t.interface({ + defaultVpc: this.defaultVpcsConfig, + endpointPolicies: t.array(this.endpointPolicyConfig), + transitGateways: t.array(this.transitGatewayConfig), + vpcs: t.array(this.vpcConfig), + vpcFlowLogs: this.vpcFlowLogsConfig, + centralNetworkServices: t.optional(this.centralNetworkServicesConfig), + dhcpOptions: t.optional(t.array(this.dhcpOptsConfig)), + vpcPeering: t.optional(t.array(this.vpcPeeringConfig)), + }); +} + +/** + * Default VPC configuration. + * Choose whether or not to delete default VPCs. + */ +export class DefaultVpcsConfig implements t.TypeOf { + /** + * Enable to delete default VPCs. + */ + readonly delete = false; + /** + * Include an array of friendly account names + * to exclude from default VPC deletion. + */ + readonly excludeAccounts = []; +} + +/** + * Transit Gateway VPC entry configuration. + * Used to define an account and VPC name for Transit Gateway static route entries. + */ +export class TransitGatewayRouteTableVpcEntryConfig + implements t.TypeOf +{ + /** + * The friendly name of the account where the VPC resides. + */ + readonly account = ''; + /** + * The friendly name of the VPC. + */ + readonly vpcName = ''; +} + +/** + * Transit Gateway route entry configuration. + * Used to define static route entries in a Transit Gateway route table. + */ +export class TransitGatewayRouteEntryConfig + implements t.TypeOf +{ + /** + * The destination CIDR block for the route table entry. + * + * @remarks + * Use CIDR notation, i.e. 10.0.0.0/16 + */ + readonly destinationCidrBlock = ''; + /** + * Enable to create a blackhole for the destination CIDR. + * Leave undefined if specifying a VPC destination. + */ + readonly blackhole: boolean | undefined = undefined; + /** + * A Transit Gateway VPC entry configuration. + * Leave undefined if specifying a blackhole destination. + * + * @see {@link TransitGatewayRouteTableVpcEntryConfig} + */ + readonly attachment: TransitGatewayRouteTableVpcEntryConfig | undefined = undefined; +} + +/** + * Transit Gateway route table configuration. + * Used to define a Transit Gateway route table. + */ +export class TransitGatewayRouteTableConfig + implements t.TypeOf +{ + /** + * A friendly name for the Transit Gateway route table. + */ + readonly name = ''; + /** + * An array of tag objects for the Transit Gateway. route table. + */ + readonly tags: t.Tag[] | undefined = undefined; + /** + * An array of Transit Gateway route entry configuration objects. + * + * @see {@link TransitGatewayRouteEntryConfig} + */ + readonly routes: TransitGatewayRouteEntryConfig[] = []; +} + +/** + * Transit Gateway configuration. + * Used to define a Transit Gateway. + */ +export class TransitGatewayConfig implements t.TypeOf { + /** + * A friendly name for the Transit Gateway. + */ + readonly name = ''; + /** + * The friendly name of the account to deploy the Transit Gateway. + */ + readonly account = ''; + /** + * The region name to deploy the Transit Gateway. + */ + readonly region = 'us-east-1'; + /** + * Resource Access Manager (RAM) share targets. + * + * @remarks + * Targets can be account names and/or organizational units. + * + * @see {@link t.ShareTargets} + */ + readonly shareTargets: t.ShareTargets = new t.ShareTargets(); + /** + * A Border Gateway Protocol (BGP) Autonomous System Number (ASN). + * + * @remarks + * The range is 64512 to 65534 for 16-bit ASNs. + * + * The range is 4200000000 to 4294967294 for 32-bit ASNs. + */ + readonly asn = 65521; + /** + * Configure DNS support between VPCs. + * + * @remarks + * Enable this option if you need the VPC to resolve public IPv4 DNS host names + * to private IPv4 addresses when queried from instances in another VPC attached + * to the transit gateway. + */ + readonly dnsSupport = 'enable'; + /** + * Equal Cost Multipath (ECMP) routing support between VPN tunnels. + * + * @remarks + * Enable this option if you need Equal Cost Multipath (ECMP) routing support between VPN tunnels. + * If connections advertise the same CIDRs, the traffic is distributed equally between them. + */ + readonly vpnEcmpSupport = 'enable'; + /** + * Configure default route table association. + * + * @remarks + * Enable this option to automatically associate transit gateway attachments with the default + * route table for the transit gateway. + */ + readonly defaultRouteTableAssociation = 'enable'; + /** + * Configure default route table propagation. + * + * @remarks + * Enable this option to automatically propagate transit gateway attachments to the default + * route table for the transit gateway. + */ + readonly defaultRouteTablePropagation = 'enable'; + /** + * Enable this option to automatically accept cross-account attachments. + */ + readonly autoAcceptSharingAttachments = 'disable'; + /** + * An array of Transit Gateway route table configuration objects. + * + * @see {@link TransitGatewayRouteTableConfig} + */ + readonly routeTables: TransitGatewayRouteTableConfig[] = []; + /** + * An array of tag objects for the Transit Gateway. + */ + readonly tags: t.Tag[] | undefined = undefined; +} + +/** + * VPC route table entry configuration. + * Used to define static route entries in a VPC route table. + */ +export class RouteTableEntryConfig implements t.TypeOf { + /** + * A friendly name for the route table. + */ + readonly name: string = ''; + /** + * The destination CIDR block for the route table entry. + * + * @remarks + * Use CIDR notation, i.e. 10.0.0.0/16 + */ + readonly destination: string = ''; + /** + * The destination type of route table entry. + * + * @see {@link NetworkConfigTypes.routeTableEntryTypeEnum} + */ + readonly type: t.TypeOf | undefined = undefined; + /** + * The friendly name of the destination target. + */ + readonly target: string = ''; + /** + * The Availability Zone (AZ) the target resides in. + * + * @remarks + * Include only the letter of the AZ name (i.e. 'a' for 'us-east-1a'). + * + * Leave undefined for targets of types other than `networkFirewall`. + */ + readonly targetAvailabilityZone: string | undefined = undefined; +} + +/** + * VPC route table configuration. + * Used to define a VPC route table. + */ +export class RouteTableConfig implements t.TypeOf { + /** + * A friendly name for the VPC route table. + */ + readonly name = ''; + /** + * An array of VPC route table entry configuration objects. + * + * @see {@link RouteTableEntryConfig} + */ + readonly routes: RouteTableEntryConfig[] = []; + /** + * An array of tag objects for the VPC route table. + */ + readonly tags: t.Tag[] = []; +} + +/** + * VPC subnet configuration. + * Used to define a VPC subnet. + */ +export class SubnetConfig implements t.TypeOf { + /** + * A friendly name for the VPC subnet. + */ + readonly name = ''; + /** + * The Availability Zone (AZ) the subnet resides in. + * + * @remarks + * Include only the letter of the AZ name (i.e. 'a' for 'us-east-1a'). + */ + readonly availabilityZone = ''; + /** + * The friendly name of the route table to associate with the subnet. + */ + readonly routeTable = ''; + /** + * The IPv4 CIDR block to associate with the subnet. + * + * @remarks + * Use CIDR notation, i.e. 10.0.0.0/16 + */ + readonly ipv4CidrBlock = ''; + /** + * Configure automatic mapping of public IPs. + * + * @remarks + * Enables you to configure the auto-assign IP settings to automatically request a public + * IPv4 address for a new network interface in this subnet. + */ + readonly mapPublicIpOnLaunch: boolean | undefined = undefined; + /** + * Resource Access Manager (RAM) share targets. + * + * @remarks + * Targets can be account names and/or organizational units. + * + * @see {@link t.ShareTargets} + */ + readonly shareTargets: t.ShareTargets = new t.ShareTargets(); + /** + * An array of tag objects for the VPC subnet. + */ + readonly tags: t.Tag[] | undefined = undefined; +} + +/** + * NAT Gateway configuration. + * Used to define an AWS-managed NAT Gateway. + */ +export class NatGatewayConfig implements t.TypeOf { + /** + * A friendly name for the NAT Gateway. + */ + readonly name = ''; + /** + * The friendly name of the subnet for the NAT Gateway to be deployed. + */ + readonly subnet = ''; + /** + * An array of tag objects for the NAT Gateway. + */ + readonly tags: t.Tag[] | undefined = undefined; +} + +/** + * Transit Gateway attachment target configuration. + * Used to define a target account for attachments. + */ +export class TransitGatewayAttachmentTargetConfig + implements t.TypeOf +{ + /** + * A friendly name for the attachment target. + */ + readonly name = ''; + /** + * The friendly name of the account for the attachment target. + */ + readonly account = ''; +} + +/** + * Transit Gateway attachment options configuration. + * Used to specify advanced options for the attachment. + */ +export class TransitGatewayAttachmentOptionsConfig + implements t.TypeOf +{ + /** + * Enable to configure appliance mode for the attachment. This option is disabled by default. + * + * @remarks + * Appliance mode ensures only a single network interface is chosen for the entirety of a traffic flow, + * enabling stateful packet inspection. + */ + readonly applianceModeSupport: t.EnableDisable | undefined = undefined; + /** + * Enable to configure DNS support for the attachment. This option is enabled by default. + */ + readonly dnsSupport: t.EnableDisable | undefined = undefined; + /** + * Enable to configure IPv6 support for the attachment. This option is disabled by default. + */ + readonly ipv6Support: t.EnableDisable | undefined = undefined; +} + +/** + * Transit Gateway attachment configuration. + * Used to define a Transit Gateway attachment. + */ +export class TransitGatewayAttachmentConfig + implements t.TypeOf +{ + /** + * A friendly name for the Transit Gateway attachment. + */ + readonly name = ''; + /** + * A Transit Gateway attachment target configuration object. + * + * @see {@link TransitGatewayAttachmentTargetConfig} + */ + readonly transitGateway: TransitGatewayAttachmentTargetConfig = new TransitGatewayAttachmentTargetConfig(); + /** + * An array of the friendly names of VPC subnets for the attachment to be deployed. + */ + readonly subnets: string[] = []; + /** + * An array of friendly names of Transit Gateway route tables to associate the attachment. + */ + readonly routeTableAssociations: string[] = []; + /** + * An array of friendly names of Transit Gateway route tables to propagate the attachment. + */ + readonly routeTablePropagations: string[] = []; + /** + * A Transit Gateway attachment options configuration. + * + * @see {@link TransitGatewayAttachmentOptionsConfig} + */ + readonly options: TransitGatewayAttachmentOptionsConfig | undefined = undefined; + /** + * An array of tag objects for the Transit Gateway attachment. + */ + readonly tags: t.Tag[] | undefined = undefined; +} + +/** + * VPC gateway endpoint service configuration. + * Used to define the service and policy for gateway endpoints. + */ +export class GatewayEndpointServiceConfig implements t.TypeOf { + /** + * The name of the service to create the endpoint for + * + * @see {@link NetworkConfigTypes.gatewayEndpointEnum} + */ + readonly service: t.TypeOf = 's3'; + /** + * The friendly name of a policy for the gateway endpoint. If left undefined, the default policy will be used. + */ + readonly policy: string | undefined = undefined; +} + +/** + * VPC gateway endpoint configuration. + * Used to define a gateway endpoints. + */ +export class GatewayEndpointConfig implements t.TypeOf { + /** + * The friendly name of the default policy for the gateway endpoints. + */ + readonly defaultPolicy: string = ''; + /** + * An array of endpoints to create. + */ + readonly endpoints: GatewayEndpointServiceConfig[] = []; +} + +/** + * VPC interface endpoint service configuration. + * Used to define the service and policy for interface endpoints. + */ +export class InterfaceEndpointServiceConfig + implements t.TypeOf +{ + /** + * The name of the service to create the endpoint for. + */ + readonly service: string = ''; + /** + * The friendly name of a policy for the interface endpoint. If left undefined, the default policy will be used. + */ + readonly policy: string | undefined = undefined; +} + +/** + * VPC interface endpoint configuration. + * Used to define interface endpoints for a VPC. + */ +export class InterfaceEndpointConfig implements t.TypeOf { + /** + * The friendly name of the default policy for the interface endpoints. + */ + readonly defaultPolicy: string = ''; + /** + * An array of VPC interface endpoint service names to be deployed. + * + * @see {@link InterfaceEndpointServiceConfig} + */ + readonly endpoints: InterfaceEndpointServiceConfig[] = [new InterfaceEndpointServiceConfig()]; + /** + * An array of the friendly names of VPC subnets for the endpoints to be deployed. + */ + readonly subnets: string[] = []; + /** + * Enable to define interface endpoints as centralized endpoints. + * + * @remarks + * Endpoints defined as central endpoints will have Route 53 private hosted zones + * created for each of them. These hosted zones are associated with any VPCs configured + * with the `useCentralEndpoints` property enabled. + */ + readonly central: boolean | undefined = undefined; + /** + * An array of source CIDRs allowed to communicate with the endpoints. + * + * @remarks + * Use CIDR notation, i.e. 10.0.0.0/16 + */ + readonly allowedCidrs: string[] | undefined = undefined; +} + +/** + * VPC subnet source configuration. + * Used to define a subnet as a source in a security group rule. + */ +export class SubnetSourceConfig implements t.TypeOf { + /** + * The friendly name of the account in which the VPC subnet resides. + */ + readonly account = ''; + /** + * The friendly name of the VPC in which the subnet resides. + */ + readonly vpc = ''; + /** + * An array of the friendly names of subnets to reference. + */ + readonly subnets: string[] = []; +} + +/** + * Security group source configuration. + * Used to define a security group as a source in a security group rule. + */ +export class SecurityGroupSourceConfig implements t.TypeOf { + /** + * An array of the friendly names of security group rules to reference. + */ + readonly securityGroups: string[] = []; +} + +/** + * Prefix list source configuration. + * Used to define a prefix list as a source in a security group rule. + */ +export class PrefixListSourceConfig implements t.TypeOf { + /** + * An array of the friendly names of prefix lists to reference. + */ + readonly prefixLists: string[] = []; +} + +/** + * Prefix list configuration. + * Used to define a custom prefix list. + */ +export class PrefixListConfig implements t.TypeOf { + /** + * A friendly name for the prefix list. + */ + readonly name = ''; + /** + * An array of friendly names for the accounts the prefix list is deployed. + */ + readonly accounts: string[] = ['']; + /** + * An array of region names for the prefix list to be deployed. + * + * @see {@link t.Region} + */ + readonly regions: t.Region[] = ['us-east-1']; + /** + * The IP address family of the prefix list. + */ + readonly addressFamily = 'IPv4'; + /** + * The maximum allowed entries in the prefix list. + */ + readonly maxEntries = 1; + /** + * An array of CIDR entries for the prefix list. + * + * @remarks + * Use CIDR notation, i.e. 10.0.0.0/16 + */ + readonly entries: string[] = []; + /** + * An array of tag objects for the prefix list. + */ + readonly tags: t.Tag[] | undefined = undefined; +} + +/** + * Security group rule configuration. + * Used to define a security group rule. + */ +export class SecurityGroupRuleConfig implements t.TypeOf { + /** + * A Description for the security group rule. + */ + readonly description = ''; + /** + * An array of protocol types to include in the security group rule. + * + * @see {@link NetworkConfigTypes.securityGroupRuleTypeEnum} + */ + readonly types = []; + /** + * An array of TCP ports to include in the security group rule. + */ + readonly tcpPorts = []; + /** + * An array of UDP ports to include in the security group rule. + */ + readonly udpPorts = []; + /** + * The port to include in the security group rule. + */ + readonly port = undefined; + /** + * The port to start from in the security group rule. + */ + readonly fromPort = undefined; + /** + * The port to end with in the security group rule. + */ + readonly toPort = undefined; + /** + * An array of sources for the security group rule. + * + * @remarks + * Valid sources are CIDR ranges, security group rules, prefix lists, and subnets. + * + * @see + * {@link SecurityGroupSourceConfig} | {@link PrefixListSourceConfig} | {@link SubnetSourceConfig} + */ + readonly sources: string[] | SecurityGroupSourceConfig[] | PrefixListSourceConfig[] | SubnetSourceConfig[] = []; +} + +/** + * Security group configuration. + * Used to define a security group. + */ +export class SecurityGroupConfig implements t.TypeOf { + /** + * The friendly name of the security group. + */ + readonly name = ''; + /** + * A description for the security group. + */ + readonly description = ''; + /** + * An array of security group rule configurations for ingress rules. + * + * @see {@link SecurityGroupRuleConfig} + */ + readonly inboundRules: SecurityGroupRuleConfig[] = []; + /** + * An array of security group rule configurations for egress rules. + * + * @see {@link SecurityGroupRuleConfig} + */ + readonly outboundRules: SecurityGroupRuleConfig[] = []; + /** + * An array of tag objects for the security group. + */ + readonly tags: t.Tag[] | undefined = undefined; +} + +/** + * Network ACL subnet selection configuration. + * Used to specify a subnet as a source for a network ACL. + */ +export class NetworkAclSubnetSelection implements t.TypeOf { + /** + * The friendly name of the account of the subnet. + */ + readonly account = ''; + /** + * The friendly name of the VPC of the subnet. + */ + readonly vpc = ''; + /** + * The friendly name of the subnet. + */ + readonly subnet = ''; +} + +/** + * Network ACL inbound rule configuration. + * Used to define an inbound rule for a network ACL. + */ +export class NetworkAclInboundRuleConfig implements t.TypeOf { + /** + * The rule ID number for the rule. + * + * @remarks + * Rules are evaluated in order from low to high. + */ + readonly rule = 100; + /** + * The protocol for the network ACL rule. + */ + readonly protocol = -1; + /** + * The port to start from in the network ACL rule. + */ + readonly fromPort = -1; + /** + * The port to end with in the network ACL rule. + */ + readonly toPort = -1; + /** + * The action for the network ACL rule. + */ + readonly action = 'allow'; + /** + * The source of the network ACL rule. + * + * @remarks + * Possible values are a CIDR range or a network ACL subnet selection configuration. + * + * @see {@link NetworkAclSubnetSelection} + */ + readonly source: string | NetworkAclSubnetSelection = ''; +} + +/** + * Network ACL outbound rule configuration. + * Used to define an outbound rule for a network ACL. + */ +export class NetworkAclOutboundRuleConfig implements t.TypeOf { + /** + * The rule ID number for the rule. + * + * @remarks + * Rules are evaluated in order from low to high. + */ + readonly rule = 100; + /** + * The protocol for the network ACL rule. + */ + readonly protocol = -1; + /** + * The port to start from in the network ACL rule. + */ + readonly fromPort = -1; + /** + * The port to end with in the network ACL rule. + */ + readonly toPort = -1; + /** + * The action for the network ACL rule. + */ + readonly action = 'allow'; + /** + * The destination of the network ACL rule. + * + * @remarks + * Possible values are a CIDR range or a network ACL subnet selection configuration. + * + * @see {@link NetworkAclSubnetSelection} + */ + readonly destination: string | NetworkAclSubnetSelection = ''; +} + +/** + * Network ACL configuration. + * Used to define the properties to configure a Network Access Control List (ACL) + */ +export class NetworkAclConfig implements t.TypeOf { + /** + * The name of the Network ACL. + * + * The value of this property will be utilized as the logical id for this + * resource. Any references to this object should specify this value. + */ + readonly name = ''; + + /** + * A list of subnets to associate with the Network ACL + */ + readonly subnetAssociations: string[] = []; + + /** + * A list of inbound rules to define for the Network ACL + * + * @see {@link NetworkAclInboundRuleConfig} + */ + readonly inboundRules: NetworkAclInboundRuleConfig[] | undefined = undefined; + + /** + * A list of outbound rules to define for the Network ACL + * + * @see {@link NetworkAclOutboundRuleConfig} + */ + readonly outboundRules: NetworkAclOutboundRuleConfig[] | undefined = undefined; + /** + * A list of tags to attach to the Network ACL + */ + readonly tags: t.Tag[] | undefined = undefined; +} + +/** + * DHCP options configuration. + * Used to define a custom DHCP options set. + */ +export class DhcpOptsConfig implements t.TypeOf { + /** + * A friendly name for the DHCP options set. + */ + readonly name: string = ''; + /** + * An array of friendly account names to deploy the options set. + */ + readonly accounts: string[] = ['']; + /** + * An array of regions to deploy the options set. + * + * @see {@link t.Region} + */ + readonly regions: t.Region[] = ['us-east-1']; + /** + * A domain name to assign to hosts using the options set. + */ + readonly domainName: string | undefined = undefined; + /** + * An array of IP addresses for domain name servers. + */ + readonly domainNameServers: string[] | undefined = undefined; + /** + * An array of IP addresses for NetBIOS servers. + */ + readonly netbiosNameServers: string[] | undefined = undefined; + /** + * The NetBIOS node type number. + * + * @see {@link NetworkConfigTypes.netbiosNodeEnum} + */ + readonly netbiosNodeType: t.TypeOf | undefined = undefined; + /** + * An array of IP addresses for NTP servers. + */ + readonly ntpServers: string[] | undefined = undefined; + /** + * An array of tags for the options set. + */ + readonly tags: t.Tag[] | undefined = undefined; +} + +/** + * VPC endpoint policy configuration. + * Used to define VPC endpoint policies. + */ +export class EndpointPolicyConfig implements t.TypeOf { + /** + * A friendly name for the endpoint policy. + */ + readonly name: string = ''; + /** + * A file path for a JSON-formatted policy document. + */ + readonly document: string = ''; +} + +/** + * VPC configuration. + * Used to define a VPC. + */ +export class VpcConfig implements t.TypeOf { + /** + * The friendly name of the VPC. + * + * The value of this property will be utilized as the logical id for this + * resource. Any references to this object should specify this value. + */ + readonly name = ''; + + /** + * The logical name of the account to deploy the VPC to + */ + readonly account = ''; + + /** + * The AWS region to deploy the VPC to + */ + readonly region = 'us-east-1'; + + /** + * A list of CIDRs to associate with the VPC. + * + * @remarks + * At least one CIDR should be + * provided. + * + * Use CIDR notation, i.e. 10.0.0.0/16 + */ + readonly cidrs: string[] = []; + + /** + * The friendly name of a DHCP options set. + */ + readonly dhcpOptions: string | undefined = undefined; + + /** + * An array of DNS firewall VPC association configurations. + * + * @see {@link NetworkConfigTypes.vpcDnsFirewallAssociationConfig} + */ + readonly dnsFirewallRuleGroups: t.TypeOf[] | undefined = + undefined; + + /** + * Defines if an internet gateway should be added to the VPC + */ + readonly internetGateway: boolean | undefined = undefined; + /** + * Enable DNS hostname support for the VPC. + */ + readonly enableDnsHostnames: boolean | undefined = true; + /** + * Enable DNS support for the VPC. + */ + readonly enableDnsSupport: boolean | undefined = true; + + /** + * Define instance tenancy for the VPC. + */ + readonly instanceTenancy: t.TypeOf | undefined = 'default'; + + /** + * An optional list of DNS query log configuration names. + */ + readonly queryLogs: string[] | undefined = undefined; + + /** + * An optional list of Route 53 resolver rule names. + */ + readonly resolverRules: string[] | undefined = undefined; + /** + * An array of route table configurations for the VPC. + */ + readonly routeTables: RouteTableConfig[] | undefined = undefined; + /** + * An array of subnet configurations for the VPC. + */ + readonly subnets: SubnetConfig[] | undefined = undefined; + /** + * An array of NAT gateway configurations for the VPC. + */ + readonly natGateways: NatGatewayConfig[] | undefined = undefined; + /** + * An array of Transit Gateway attachment configurations. + */ + readonly transitGatewayAttachments: TransitGatewayAttachmentConfig[] | undefined = undefined; + + /** + * An array of gateway endpoints for the VPC. + */ + readonly gatewayEndpoints: GatewayEndpointConfig | undefined = undefined; + + /** + * A list of VPC interface endpoints. + */ + readonly interfaceEndpoints: InterfaceEndpointConfig | undefined = undefined; + + /** + * When set to true, this VPC will be configured to utilize centralized + * endpoints. This includes having the Route 53 Private Hosted Zone + * associated with this VPC. Centralized endpoints are configured per + * region, and can span to spoke accounts + * + * @default false + */ + readonly useCentralEndpoints: boolean | undefined = false; + + /** + * A list of Security Groups to deploy for this VPC + * + * @default undefined + */ + readonly securityGroups: SecurityGroupConfig[] | undefined = undefined; + + /** + * A list of Prefix Lists to deploy for this VPC + * + * @default undefined + */ + readonly prefixLists: PrefixListConfig[] | undefined = undefined; + + /** + * A list of Network Access Control Lists (ACLs) to deploy for this VPC + * + * @default undefined + */ + readonly networkAcls: NetworkAclConfig[] | undefined = undefined; + + /** + * A list of tags to apply to this VPC + * + * @default undefined + * + */ + readonly tags: t.Tag[] | undefined = undefined; +} + +/** + * VPC flow logs configuration. + * Used to customize VPC flow log output. + */ +export class VpcFlowLogsConfig implements t.TypeOf { + /** + * The type of traffic to log. + * + * @see {@link NetworkConfigTypes.trafficTypeEnum} + */ + readonly trafficType = 'ALL'; + /** + * The maximum log aggregation interval in days. + */ + readonly maxAggregationInterval: number = 600; + /** + * An array of destination serviced for storing logs. + * + * @see {@link NetworkConfigTypes.logDestinationTypeEnum} + */ + readonly destinations: t.TypeOf[] = ['s3', 'cloud-watch-logs']; + /** + * Enable to use the default log format for flow logs. + */ + readonly defaultFormat = false; + /** + * Custom fields to include in flow log outputs. + */ + readonly customFields = [ + 'version', + 'account-id', + 'interface-id', + 'srcaddr', + 'dstaddr', + 'srcport', + 'dstport', + 'protocol', + 'packets', + 'bytes', + 'start', + 'end', + 'action', + 'log-status', + 'vpc-id', + 'subnet-id', + 'instance-id', + 'tcp-flags', + 'type', + 'pkt-srcaddr', + 'pkt-dstaddr', + 'region', + 'az-id', + 'pkt-src-aws-service', + 'pkt-dst-aws-service', + 'flow-direction', + 'traffic-path', + ]; +} + +/** + * Route 53 resolver rule configuration. + * Used to define resolver rules. + */ +export class ResolverRuleConfig implements t.TypeOf { + /** + * A friendly name for the resolver rule. + */ + readonly name: string = ''; + /** + * The domain name for the resolver rule. + */ + readonly domainName: string = ''; + /** + * The friendly name of an inbound endpoint to target. + * + * @remarks + * Use this property to define resolver rules for resolving DNS records across subdomains + * hosted within the accelerator environment. + */ + readonly inboundEndpointTarget: string | undefined = undefined; + /** + * The type of rule to create. + * + * @see {@link NetworkConfigTypes.ruleTypeEnum} + */ + readonly ruleType: t.TypeOf | undefined = 'FORWARD'; + /** + * Resource Access Manager (RAM) share targets. + * + * @remarks + * Targets can be account names and/or organizational units. + * + * @see {@link t.ShareTargets} + */ + readonly shareTargets: t.ShareTargets | undefined = undefined; + /** + * An array of tags for the resolve rule. + */ + readonly tags: t.Tag[] | undefined = undefined; + /** + * An array of target IP configurations for the resolver rule. + * + * @see {@link NetworkConfigTypes.ruleTargetIps} + */ + readonly targetIps: t.TypeOf[] | undefined = undefined; +} + +/** + * Route 53 resolver endpoint configuration. + * Used to define a resolver endpoint. + */ +export class ResolverEndpointConfig implements t.TypeOf { + /** + * The friendly name of the resolver endpoint. + */ + readonly name: string = ''; + /** + * The type of resolver endpoint to deploy. + * + * @see {@link NetworkConfigTypes.resolverEndpointTypeEnum} + */ + readonly type: t.TypeOf = 'INBOUND'; + /** + * The friendly name of the VPC to deploy the resolver endpoint to. + */ + readonly vpc: string = ''; + /** + * An array of friendly names for subnets to deploy the resolver endpoint to. + */ + readonly subnets: string[] = []; + /** + * The allowed ingress/egress CIDRs for the resolver endpoint security group. + */ + readonly allowedCidrs: string[] | undefined = undefined; + /** + * An array of friendly names of the resolver rules to associate with the endpoint. + * + * @see {@link ResolverRuleConfig} + */ + readonly rules: ResolverRuleConfig[] | undefined = undefined; + /** + * An array of tags for the resolver endpoint. + */ + readonly tags: t.Tag[] | undefined = undefined; +} + +/** + * Route 53 DNS query logging configuration. + * Use to define query logging configs. + */ +export class DnsQueryLogsConfig implements t.TypeOf { + /** + * The friendly name of the query logging config. + */ + readonly name: string = ''; + /** + * An array of destination services used to store the logs. + * + * @see {@link NetworkConfigTypes.logDestinationTypeEnum} + */ + readonly destinations: t.TypeOf[] = ['s3']; + /** + * Resource Access Manager (RAM) share targets. + * + * @remarks + * Targets can be account names and/or organizational units. + * + * @see {@link t.ShareTargets} + */ + readonly shareTargets: t.ShareTargets | undefined = undefined; +} + +/** + * Route 53 DNS firewall rule configuration. + * Used to define DNS firewall rules. + */ +export class DnsFirewallRulesConfig implements t.TypeOf { + /** + * A friendly name for the DNS firewall rule. + */ + readonly name: string = ''; + /** + * An action for the DNS firewall rule to take on matching requests. + * + * @see {@link NetworkConfigTypes.dnsFirewallRuleActionTypeEnum} + */ + readonly action: t.TypeOf = 'ALERT'; + /** + * The priority of the DNS firewall rule. + * + * @remarks + * Rules are evaluated in order from low to high number. + */ + readonly priority: number = 100; + /** + * Configure an override domain for BLOCK actions. + */ + readonly blockOverrideDomain: string | undefined = undefined; + /** + * Configure a time-to-live (TTL) for the override domain. + */ + readonly blockOverrideTtl: number | undefined = undefined; + /** + * Configure a specific response type for BLOCK actions. + * + * @see {@link NetworkConfigTypes.dnsFirewallBlockResponseTypeEnum} + */ + readonly blockResponse: t.TypeOf | undefined = undefined; + /** + * A file containing a custom domain list in TXT format. + */ + readonly customDomainList: string | undefined = undefined; + /** + * Configure a rule that uses an AWS-managed domain list. + * + * @see {@link NetworkConfigTypes.dnsFirewallManagedDomainListEnum} + */ + readonly managedDomainList: t.TypeOf | undefined = + undefined; +} + +/** + * Route 53 DNS firewall rule group configuration. + * Used to define a DNS firewall rule group. + */ +export class DnsFirewallRuleGroupConfig implements t.TypeOf { + /** + * A friendly name for the DNS firewall rule group. + */ + readonly name: string = ''; + /** + * The regions to deploy the rule group to. + * + * @see {@link t.Region} + */ + readonly regions: t.Region[] = ['us-east-1']; + /** + * An array of DNS firewall rule configurations. + * + * @see {@link DnsFirewallRulesConfig} + */ + readonly rules: DnsFirewallRulesConfig[] = []; + /** + * Resource Access Manager (RAM) share targets. + * + * @remarks + * Targets can be account names and/or organizational units. + * + * @see {@link t.ShareTargets} + */ + readonly shareTargets: t.ShareTargets | undefined = undefined; + /** + * An array of tags for the rule group. + */ + readonly tags: t.Tag[] | undefined = undefined; +} + +/** + * Route 53 resolver configuration. + * Used to define configurations for Route 53 resolver. + */ +export class ResolverConfig implements t.TypeOf { + /** + * An array of Route 53 resolver endpoint configurations. + * + * @see {@link ResolverEndpointConfig} + */ + readonly endpoints: ResolverEndpointConfig[] | undefined = undefined; + /** + * An array of Route 53 DNS firewall rule group configurations. + * + * @see {@link DnsFirewallRuleGroupConfig} + */ + readonly firewallRuleGroups: DnsFirewallRuleGroupConfig[] | undefined = undefined; + /** + * A Route 53 resolver DNS query logging configuration. + * + * @see {@link DnsQueryLogsConfig} + */ + readonly queryLogs: DnsQueryLogsConfig | undefined = undefined; +} + +/** + * Network Firewall rule source list configuration. + * Used to define DNS allow and deny lists for Network Firewall. + * + * @see {@link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-rulegroup-rulessourcelist.html} + */ +export class NfwRuleSourceListConfig implements t.TypeOf { + /** + * The type of rules to generate from the source list. + * + * @see {@link NetworkConfigTypes.nfwGeneratedRulesType} + */ + readonly generatedRulesType: t.TypeOf = 'DENYLIST'; + /** + * An array of target domain names. + * + * @remarks + * Supported values are as fallows: + * Explicit domain names such as `www.example.com`. + * Wildcard domain names should be prefaced with a `.`. For example: `.example.com` + */ + readonly targets: string[] = []; + /** + * An array of protocol types to inspect. + * + * @see {@link NetworkConfigTypes.nfwTargetType} + */ + readonly targetTypes: t.TypeOf[] = ['TLS_SNI']; +} + +/** + * Network Firewall stateful rule header configuration. + * Used to specify a stateful rule in a header-type format. + * + * @see {@link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-rulegroup-header.html} + */ +export class NfwRuleSourceStatefulRuleHeaderConfig + implements t.TypeOf +{ + /** + * The destination CIDR range to inspect for. + * + * @remarks + * Use CIDR notation, i.e. 10.0.0.0/16 + */ + readonly destination: string = ''; + /** + * The destination port or port range to inspect. + * + * @remarks + * To specify a port range, separate the values with a colon `:`. + * For example: `80:443`. To specify all ports, use `ANY`. + */ + readonly destinationPort: string = ''; + /** + * The direction of the traffic flow to inspect. + * + * @remarks + * Use `ANY` to match bidirectional traffic. + * + * Use `FORWARD` to match only traffic going from the source to destination. + * + * @see {@link NetworkConfigTypes.nfwStatefulRuleDirectionType} + */ + readonly direction: t.TypeOf = 'ANY'; + /** + * The protocol to inspect. + * + * @remarks + * To specify all traffic, use `IP`. + * + * @see {@link NetworkConfigTypes.nfwStatefulRuleProtocolType} + */ + readonly protocol: t.TypeOf = 'IP'; + /** + * The source CIDR range to inspect for. + * + * @remarks + * Use CIDR notation, i.e. 10.0.0.0/16 + */ + readonly source: string = ''; + /** + * The source port or port range to inspect. + * + * @remarks + * To specify a port range, separate the values with a colon `:`. + * For example: `80:443`. To specify all ports, use `ANY`. + */ + readonly sourcePort: string = ''; +} + +/** + * Network Firewall stateful rule options configuration. + * Use to specify keywords and settings for stateful rules. + * + * @see {@link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-rulegroup-ruleoption.html} + */ +export class NfwRuleSourceStatefulRuleOptionsConfig + implements t.TypeOf +{ + /** + * A Suricata-compatible keyword. + */ + readonly keyword: string = ''; + /** + * An array of values for the keyword. + */ + readonly settings: string[] | undefined = undefined; +} + +/** + * Network Firewall stateful rule configuration. + * Use to define stateful rules for Network Firewall. + * + * @see {@link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-rulegroup-statefulrule.html} + */ +export class NfwRuleSourceStatefulRuleConfig + implements t.TypeOf +{ + /** + * The action type for the stateful rule. + * + * @see {@link NetworkConfigTypes.nfwStatefulRuleActionType} + */ + readonly action: t.TypeOf = 'DROP'; + /** + * A Network Firewall stateful rule header configuration. + * + * @see {@link NfwRuleSourceStatefulRuleHeaderConfig} + */ + readonly header: NfwRuleSourceStatefulRuleHeaderConfig = new NfwRuleSourceStatefulRuleHeaderConfig(); + /** + * An array of Network Firewall stateful rule options configurations. + * + * @see {@link NfwRuleSourceStatefulRuleOptionsConfig} + */ + readonly ruleOptions: NfwRuleSourceStatefulRuleOptionsConfig[] = [new NfwRuleSourceStatefulRuleOptionsConfig()]; +} + +/** + * Network Firewall custom actions dimensions. + * Used to define custom actions to log in CloudWatch metrics. + * + * @see {@link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-rulegroup-dimension.html} + */ +export class NfwRuleSourceCustomActionDimensionConfig + implements t.TypeOf +{ + /** + * An array of values of the custom metric dimensions to log. + */ + readonly dimensions: string[] = []; +} + +/** + * Network Firewall custom action definition configuration. + * Used to define custom metrics for Network Firewall. + * + * @see {@link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-rulegroup-actiondefinition.html} + */ +export class NfwRuleSourceCustomActionDefinitionConfig + implements t.TypeOf +{ + /** + * A Network Firewall custom action dimensions configuration. + * + * @see {@link NfwRuleSourceCustomActionDimensionConfig} + */ + readonly publishMetricAction: NfwRuleSourceCustomActionDimensionConfig = + new NfwRuleSourceCustomActionDimensionConfig(); +} + +/** + * Network Firewall custom action configuration. + * Used to define custom actions for Network Firewall. + * + * @see {@link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-rulegroup-customaction.html} + */ +export class NfwRuleSourceCustomActionConfig + implements t.TypeOf +{ + /** + * A Network Firewall custom action definition configuration. + * + * @see {@link NfwRuleSourceCustomActionDefinitionConfig} + */ + readonly actionDefinition: NfwRuleSourceCustomActionDefinitionConfig = + new NfwRuleSourceCustomActionDefinitionConfig(); + /** + * A friendly name for the custom action. + */ + readonly actionName: string = ''; +} + +/** + * Network Firewall stateless port range configuration. + * Used to define a port range in stateless rules. + * + * @see {@link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-rulegroup-portrange.html} + */ +export class NfwRuleSourceStatelessPortRangeConfig + implements t.TypeOf +{ + /** + * The port to start from in the range. + */ + readonly fromPort: number = 123; + /** + * The port to end with in the range. + */ + readonly toPort: number = 123; +} + +/** + * Network Firewall stateless TCP flags configuration. + * Used to define TCP flags to inspect in stateless rules. + * + * @see {@link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-rulegroup-tcpflagfield.html} + */ +export class NfwRuleSourceStatelessTcpFlagsConfig + implements t.TypeOf +{ + /** + * An array of TCP flags. + * + * @remarks + * Used in conjunction with the Masks setting to define the flags that must be set + * and flags that must not be set in order for the packet to match. + * This setting can only specify values that are also specified in the Masks setting. + */ + readonly flags: string[] = []; + /** + * The set of flags to consider in the inspection. + * + * @remarks + * For the flags that are specified in the masks setting, the following must be true + * for the packet to match: + * The ones that are set in this flags setting must be set in the packet. + * The ones that are not set in this flags setting must also not be set in the packet. + * To inspect all flags in the valid values list, leave this with no setting. + */ + readonly masks: string[] | undefined = undefined; +} + +/** + * Network Firewall stateless rule match attributes configuration. + * Used to define stateless rule match attributes for Network Firewall. + * + * @see {@link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-rulegroup-matchattributes.html} + */ +export class NfwRuleSourceStatelessMatchAttributesConfig + implements t.TypeOf +{ + /** + * An array of Network Firewall stateless port range configurations. + * + * @see {@link NfwRuleSourceStatelessPortRangeConfig} + */ + readonly destinationPorts: NfwRuleSourceStatelessPortRangeConfig[] = [new NfwRuleSourceStatelessPortRangeConfig()]; + /** + * An array of destination CIDR ranges. + * + * @remarks + * Use CIDR notation, i.e. 10.0.0.0/16 + */ + readonly destinations: string[] = []; + /** + * An array of IP protocol numbers to inspect. + */ + readonly protocols: number[] = []; + /** + * An array of Network Firewall stateless port range configurations. + * + * @see {@link NfwRuleSourceStatelessPortRangeConfig} + */ + readonly sourcePorts: NfwRuleSourceStatelessPortRangeConfig[] = [new NfwRuleSourceStatelessPortRangeConfig()]; + /** + * An array of source CIDR ranges. + * + * @remarks + * Use CIDR notation, i.e. 10.0.0.0/16 + */ + readonly sources: string[] = []; + /** + * An array of Network Firewall stateless TCP flag configurations. + * + * @see {@link NfwRuleSourceStatelessTcpFlagsConfig} + */ + readonly tcpFlags: NfwRuleSourceStatelessTcpFlagsConfig[] = [new NfwRuleSourceStatelessTcpFlagsConfig()]; +} + +/** + * Network Firewall stateless rule definition configuration. + * Used to define a stateless rule definition. + * + * @see {@link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-rulegroup-ruledefinition.html} + */ +export class NfwRuleSourceStatelessRuleDefinitionConfig + implements t.TypeOf +{ + /** + * An array of actions to take using the stateless rule engine. + * + * @see {@link NetworkConfigTypes.nfwStatelessRuleActionType} + */ + readonly actions: t.TypeOf[] = ['aws:drop']; + /** + * A Network Firewall stateless rule match attributes configuration. + * + * @see {@link NfwRuleSourceStatelessMatchAttributesConfig} + */ + readonly matchAttributes: NfwRuleSourceStatelessMatchAttributesConfig = + new NfwRuleSourceStatelessMatchAttributesConfig(); +} + +/** + * Network Firewall stateless rule configuration. + * Used to define a stateless rule for Network Firewall. + * + * @see {@link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-rulegroup-statelessrule.html} + */ +export class NfwRuleSourceStatelessRuleConfig + implements t.TypeOf +{ + /** + * The priority number for the rule. + * + * @remarks + * Priority is evaluated in order from low to high. + */ + readonly priority: number = 123; + /** + * A Network Firewall stateless rule definition configuration. + * + * @see {@link NfwRuleSourceStatelessRuleDefinitionConfig} + */ + readonly ruleDefinition: NfwRuleSourceStatelessRuleDefinitionConfig = + new NfwRuleSourceStatelessRuleDefinitionConfig(); +} + +/** + * Network Firewall stateless rules and custom metrics configuration. + * Used to define stateless rules and/or custom metrics for Network Firewall. + * + * @see {@link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-rulegroup-statelessrulesandcustomactions.html} + */ +export class NfwStatelessRulesAndCustomActionsConfig + implements t.TypeOf +{ + /** + * An array of Network Firewall stateless rule configurations. + * + * @see {@link NfwRuleSourceStatelessRuleConfig} + */ + readonly statelessRules: NfwRuleSourceStatelessRuleConfig[] = [new NfwRuleSourceStatelessRuleConfig()]; + /** + * An array of Network Firewall custom action configurations. + * + * @see {@link NfwRuleSourceCustomActionConfig} + */ + readonly customActions: NfwRuleSourceCustomActionConfig[] | undefined = undefined; +} + +/** + * Network Firewall rule source configuration. + * Used to define rules for a Network Firewall. + * + * @see {@link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-rulegroup-rulessource.html} + */ +export class NfwRuleSourceConfig implements t.TypeOf { + /** + * A Network Firewall rule source list configuration. + * + * @see {@link NfwRuleSourceListConfig} + */ + readonly rulesSourceList: NfwRuleSourceListConfig | undefined = undefined; + /** + * A Suricata-compatible stateful rule string. + * + * @see {@link https://docs.aws.amazon.com/network-firewall/latest/developerguide/suricata-examples.html#suricata-example-rule-with-variables} + */ + readonly rulesString: string | undefined = undefined; + /** + * An array of Network Firewall stateful rule configurations. + * + * @see {@link NfwRuleSourceStatefulRuleConfig} + */ + readonly statefulRules: NfwRuleSourceStatefulRuleConfig[] | undefined = undefined; + /** + * A Network Firewall stateless rules and custom action configuration. + * + * @see {@link NfwStatelessRulesAndCustomActionsConfig} + */ + readonly statelessRulesAndCustomActions: NfwStatelessRulesAndCustomActionsConfig | undefined = undefined; +} + +/** + * Network Firewall rule variable definition configuration. + * Used to define a rule variable definition for Network Firewall. + * + * @see {@link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-rulegroup-rulevariables.html} + */ +export class NfwRuleVariableDefinitionConfig + implements t.TypeOf +{ + /** + * A name for the rule variable. + */ + readonly name: string = ''; + /** + * An array of values for the rule variable. + */ + readonly definition: string[] = []; +} + +/** + * Network Firewall rule variable configuration. + * Used to define a rule variable for Network Firewall. + * + * @see {@link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-rulegroup-rulevariables.html} + */ +export class NfwRuleVariableConfig implements t.TypeOf { + /** + * A Network Firewall rule variable definition configuration. + * + * @see {@link NfwRuleVariableDefinitionConfig} + */ + readonly ipSets: NfwRuleVariableDefinitionConfig = new NfwRuleVariableDefinitionConfig(); + /** + * A Network Firewall rule variable definition configuration. + * + * @see {@link NfwRuleVariableDefinitionConfig} + */ + readonly portSets: NfwRuleVariableDefinitionConfig = new NfwRuleVariableDefinitionConfig(); +} + +/** + * Network Firewall rule group rule configuration. + * Used to define rules for a Network Firewall rule group. + * + * @see {@link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-rulegroup-rulegroup.html} + */ +export class NfwRuleGroupRuleConfig implements t.TypeOf { + /** + * A Network Firewall rule source configuration. + * + * @see {@link NfwRuleSourceConfig} + */ + readonly rulesSource: NfwRuleSourceConfig = new NfwRuleSourceConfig(); + /** + * A Network Firewall rule variable configuration. + * + * @see {@link NfwRuleVariableConfig} + */ + readonly ruleVariables: NfwRuleVariableConfig | undefined = undefined; + /** + * A stateful rule option for the rule group. + * + * @see {@link NetworkConfigTypes.nfwStatefulRuleOptionsType} + */ + readonly statefulRuleOptions: t.TypeOf | undefined = undefined; +} + +/** + * Network Firewall rule group configuration. + * Used to define a rule group for Network Firewall. + * + * @see {@link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-networkfirewall-rulegroup.html} + */ +export class NfwRuleGroupConfig implements t.TypeOf { + /** + * A friendly name for the rule group. + */ + readonly name: string = ''; + /** + * The regions to deploy the rule group to. + * + * @see {@link t.Region} + */ + readonly regions: t.Region[] = []; + /** + * The capacity of the rule group. + */ + readonly capacity: number = 123; + /** + * The type of rules in the rule group. + * + * @see {@link NetworkConfigTypes.nfwRuleType} + */ + readonly type: t.TypeOf = 'STATEFUL'; + /** + * A description for the rule group. + */ + readonly description: string | undefined = undefined; + /** + * A Network Firewall rule group configuration. + * + * @see {@link NfwRuleGroupRuleConfig} + */ + readonly ruleGroup: NfwRuleGroupRuleConfig | undefined = undefined; + /** + * Resource Access Manager (RAM) share targets. + * + * @remarks + * Targets can be account names and/or organizational units. + * + * @see {@link t.ShareTargets} + */ + readonly shareTargets: t.ShareTargets | undefined = undefined; + /** + * An array of tags for the rule group. + */ + readonly tags: t.Tag[] | undefined = undefined; +} + +/** + * Network Firewall stateful rule group reference configuration. + * Used to reference a stateful rule group in a Network Firewall policy. + * + * @see {@link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-firewallpolicy-statefulrulegroupreference.html} + */ +export class NfwStatefulRuleGroupReferenceConfig + implements t.TypeOf +{ + /** + * The friendly name of the rule group. + */ + readonly name: string = ''; + /** + * If using strict ordering, a priority number for the rule. + */ + readonly priority: number | undefined = undefined; +} + +/** + * Network Firewall stateless rule group configuration. + * Used to reference a stateless rule group in a Network Firewall policy. + * + * @see {@link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-firewallpolicy-statelessrulegroupreference.html} + */ +export class NfwStatelessRuleGroupReferenceConfig + implements t.TypeOf +{ + /** + * The friendly name of the rule group. + */ + readonly name: string = ''; + /** + * A priority number for the rule. + */ + readonly priority: number = 123; +} + +/** + * Network Firewall policy policy configuration. + * Used to define the configuration of a Network Firewall policy. + * + * @see {@link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-firewallpolicy-firewallpolicy.html} + */ +export class NfwFirewallPolicyPolicyConfig + implements t.TypeOf +{ + /** + * An array of default actions to take on packets evaluated by the stateless engine. + */ + readonly statelessDefaultActions: string[] = []; + /** + * An array of default actions to take on fragmented packets. + */ + readonly statelessFragmentDefaultActions: string[] = []; + /** + * An array of default actions to take on packets evaluated by the stateful engine. + */ + readonly statefulDefaultActions: string[] | undefined = undefined; + /** + * Define how the stateful engine will evaluate packets. + * + * @see {@link NetworkConfigTypes.nfwStatefulRuleOptionsType} + */ + readonly statefulEngineOptions: t.TypeOf | undefined = + undefined; + /** + * An array of Network Firewall stateful rule group reference configurations. + * + * @see {@link NfwStatefulRuleGroupReferenceConfig} + */ + readonly statefulRuleGroups: NfwStatefulRuleGroupReferenceConfig[] | undefined = undefined; + /** + * An array of Network Firewall custom action configurations. + * + * @see {@link NfwRuleSourceCustomActionConfig} + */ + readonly statelessCustomActions: NfwRuleSourceCustomActionConfig[] | undefined = undefined; + /** + * An array of Network Firewall stateless rule group reference configurations. + * + * @see {@link NfwStatelessRuleGroupReferenceConfig} + */ + readonly statelessRuleGroups: NfwStatelessRuleGroupReferenceConfig[] | undefined = undefined; +} + +/** + * Network Firewall policy configuration. + * Used to define a Network Firewall policy. + * + * @see {@link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-networkfirewall-firewallpolicy.html} + */ +export class NfwFirewallPolicyConfig implements t.TypeOf { + /** + * A friendly name for the policy. + */ + readonly name: string = ''; + /** + * A Network Firewall policy policy configuration. + * + * @see {@link NfwFirewallPolicyPolicyConfig} + */ + readonly firewallPolicy: NfwFirewallPolicyPolicyConfig = new NfwFirewallPolicyPolicyConfig(); + /** + * The regions to deploy the policy to. + * + * @see {@link t.Region} + */ + readonly regions: t.Region[] = []; + /** + * A description for the policy. + */ + readonly description: string | undefined = undefined; + /** + * Resource Access Manager (RAM) share targets. + * + * @remarks + * Targets can be account names and/or organizational units. + * + * @see {@link t.ShareTargets} + */ + readonly shareTargets: t.ShareTargets | undefined = undefined; + /** + * An array of tags for the policy. + */ + readonly tags: t.Tag[] | undefined = undefined; +} + +/** + * Network Firewall logging configuration. + * Used to define logging destinations for Network Firewall. + * + * @see {@link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-loggingconfiguration-logdestinationconfig.html} + */ +export class NfwLoggingConfig implements t.TypeOf { + /** + * The destination service to log to. + * + * @see {@link NetworkConfigTypes.logDestinationTypeEnum} + */ + readonly destination: t.TypeOf = 's3'; + /** + * The type of actions to log. + * + * @see {@link NetworkConfigTypes.nfwLogType} + */ + readonly type: t.TypeOf = 'ALERT'; +} + +/** + * Network Firewall firewall configuration. + * Used to define a Network Firewall firewall. + * + * @see {@link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-networkfirewall-firewall.html} + */ +export class NfwFirewallConfig implements t.TypeOf { + /** + * A friendly name for the firewall. + */ + readonly name: string = ''; + /** + * The friendly name of the Network Firewall policy. + */ + readonly firewallPolicy: string = ''; + /** + * An array of the friendly names of subnets to deploy Network Firewall to. + */ + readonly subnets: string[] = []; + /** + * The friendly name of the VPC to deploy Network Firewall to. + */ + readonly vpc: string = ''; + /** + * Enable for deletion protection on the firewall. + */ + readonly deleteProtection: boolean | undefined = undefined; + /** + * A description for the firewall. + */ + readonly description: string | undefined = undefined; + /** + * Enable to disallow firewall policy changes. + */ + readonly firewallPolicyChangeProtection: boolean | undefined = undefined; + /** + * Enable to disallow firewall subnet changes. + */ + readonly subnetChangeProtection: boolean | undefined = undefined; + /** + * An array of Network Firewall logging configurations. + * + * @see {@link NfwLoggingConfig} + */ + readonly loggingConfiguration: NfwLoggingConfig[] | undefined = undefined; + /** + * An array of tags for the firewall. + */ + readonly tags: t.Tag[] | undefined = undefined; +} + +/** + * Network Firewall configuration. + * Used to define Network Firewall configurations for the accelerator. + */ +export class NfwConfig implements t.TypeOf { + /** + * An array of Network Firewall firewall configurations. + * + * @see {@link NfwFirewallConfig} + */ + readonly firewalls: NfwFirewallConfig[] = []; + /** + * An array of Network Firewall policy configurations. + * + * @see {@link NfwFirewallPolicyConfig} + */ + readonly policies: NfwFirewallPolicyConfig[] = []; + /** + * An array of Network Firewall rule group configurations. + * + * @see {@link NfwRuleGroupConfig} + */ + readonly rules: NfwRuleGroupConfig[] = []; +} + +/** + * Central network services configuration. + * Used to define centralized networking services for the accelerator. + */ +export class CentralNetworkServicesConfig implements t.TypeOf { + /** + * The friendly name of the delegated administrator account for network services. + */ + readonly delegatedAdminAccount: string = ''; + /** + * A Route 53 resolver configuration. + * + * @see {@link ResolverConfig} + */ + readonly route53Resolver: ResolverConfig | undefined = undefined; + /** + * A Network Firewall configuration. + * + * @see {@link NfwConfig} + */ + readonly networkFirewall: NfwConfig | undefined = undefined; +} + +/** + * VPC peering configuration. + * Used to define VPC peering connections. + */ +export class VpcPeeringConfig implements t.TypeOf { + /** + * A friendly name for the peering connection. + */ + readonly name: string = ''; + /** + * The VPCs to peer. + */ + readonly vpcs: string[] = []; + /** + * An array of tags for the peering connection. + */ + readonly tags: t.Tag[] | undefined = undefined; +} + +/** + * Network Configuration. + * Used to define a network configuration for the accelerator. + */ +export class NetworkConfig implements t.TypeOf { + /** + * The name of the network configuration file. + */ + static readonly FILENAME = 'network-config.yaml'; + + /** + * A default VPC configuration. + * + * @see {@link DefaultVpcsConfig} + */ + readonly defaultVpc: DefaultVpcsConfig = new DefaultVpcsConfig(); + + /** + * An array of Transit Gateway configurations. + * + * @see {@link TransitGatewayConfig} + */ + readonly transitGateways: TransitGatewayConfig[] = []; + + /** + * An array of VPC endpoint policies. + * + * @see {@link EndpointPolicyConfig} + */ + readonly endpointPolicies: EndpointPolicyConfig[] = []; + + /** + * An array of VPC configurations. + * + * @see {@link VpcConfig} + */ + readonly vpcs: VpcConfig[] = []; + + /** + * A VPC flow logs configuration. + * + * @see {@link VpcFlowLogsConfig} + */ + readonly vpcFlowLogs: VpcFlowLogsConfig = new VpcFlowLogsConfig(); + + /** + * An optional list of DHCP options set configurations. + * + * @see {@link DhcpOptsConfig} + */ + readonly dhcpOptions: DhcpOptsConfig[] | undefined = undefined; + + /** + * An optional Central Network services configuration. + * + * @see {@link CentralNetworkServicesConfig} + */ + readonly centralNetworkServices: CentralNetworkServicesConfig | undefined = undefined; + + /** + * An optional list of VPC peering configurations + * + * @see {@link VpcPeeringConfig} + */ + readonly vpcPeering: VpcPeeringConfig[] | undefined = undefined; + + /** + * + * @param values + */ + constructor(values?: t.TypeOf) { + if (values) { + Object.assign(this, values); + } + } + + /** + * An optional list of prefix list set configurations. + */ + readonly prefixLists: PrefixListConfig[] | undefined = undefined; + + /** + * + * @param dir + * @returns + */ + static load(dir: string): NetworkConfig { + const buffer = fs.readFileSync(path.join(dir, NetworkConfig.FILENAME), 'utf8'); + const values = t.parse(NetworkConfigTypes.networkConfig, yaml.load(buffer)); + return new NetworkConfig(values); + } + + /** + * Load from string content + * @param content + */ + static loadFromString(content: string): NetworkConfig | undefined { + try { + const values = t.parse(NetworkConfigTypes.networkConfig, yaml.load(content)); + return new NetworkConfig(values); + } catch (e) { + console.log('[network-config] Error parsing input, global config undefined'); + console.log(`${e}`); + return undefined; + } + } +} diff --git a/source/packages/@aws-accelerator/config/lib/organization-config.ts b/source/packages/@aws-accelerator/config/lib/organization-config.ts new file mode 100644 index 000000000..12e426c82 --- /dev/null +++ b/source/packages/@aws-accelerator/config/lib/organization-config.ts @@ -0,0 +1,514 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { throttlingBackOff } from '@aws-accelerator/utils'; +import * as AWS from 'aws-sdk'; +import * as fs from 'fs'; +import * as yaml from 'js-yaml'; +import * as path from 'path'; +import * as t from './common-types'; + +/** + * AWS Organizations configuration items. + */ +export abstract class OrganizationConfigTypes { + static readonly organizationalUnitConfig = t.interface({ + name: t.nonEmptyString, + }); + + static readonly organizationalUnitIdConfig = t.interface({ + name: t.nonEmptyString, + id: t.nonEmptyString, + arn: t.nonEmptyString, + }); + + static readonly quarantineNewAccountsConfig = t.interface({ + enable: t.boolean, + scpPolicyName: t.optional(t.nonEmptyString), + }); + + static readonly serviceControlPolicyConfig = t.interface({ + name: t.nonEmptyString, + description: t.nonEmptyString, + policy: t.nonEmptyString, + type: t.enums('Type', ['awsManaged', 'customerManaged'], 'Value should be a Service Control Policy Type'), + deploymentTargets: t.deploymentTargets, + }); + + static readonly tagPolicyConfig = t.interface({ + name: t.nonEmptyString, + description: t.nonEmptyString, + policy: t.nonEmptyString, + deploymentTargets: t.deploymentTargets, + }); + + static readonly backupPolicyConfig = t.interface({ + name: t.nonEmptyString, + description: t.nonEmptyString, + policy: t.nonEmptyString, + deploymentTargets: t.deploymentTargets, + }); + + static readonly organizationConfig = t.interface({ + enable: t.boolean, + organizationalUnits: t.array(this.organizationalUnitConfig), + organizationalUnitIds: t.optional(t.array(this.organizationalUnitIdConfig)), + serviceControlPolicies: t.array(this.serviceControlPolicyConfig), + taggingPolicies: t.array(this.tagPolicyConfig), + backupPolicies: t.array(this.backupPolicyConfig), + }); +} + +/** + * AWS Organizational Unit (OU) configuration + */ +export abstract class OrganizationalUnitConfig + implements t.TypeOf +{ + /** + * The name and nested path that you want to assign to the OU. + * When referring to OU's in the other configuration files ensure + * that the name matches what has been provided here. + * For example if you wanted an OU directly off of root just supply the OU name. + * Always configure all of the OUs in the path. + * A nested OU configuration would be like this + * - name: Sandbox + * - name: Sandbox/Pipeline + * - name: Sandbox/Development + * - name: Sandbox/Development/Application1 + */ + readonly name: string = ''; +} + +/** + * Organizational unit in configuration + */ +export abstract class OrganizationalUnitIdConfig + implements t.TypeOf +{ + /** + * A name for the OU + */ + readonly name: string = ''; + /** + * OU id + */ + readonly id: string = ''; + /** + * OU arn + */ + readonly arn: string = ''; +} + +/** + * Quarantine SCP application configuration + */ +export abstract class QuarantineNewAccountsConfig + implements t.TypeOf +{ + /** + * Indicates where or not a Quarantine policy is applied + * when new accounts are created. If enabled all accounts created by + * any means will have the configured policy applied. + */ + readonly enable: boolean = true; + /** + * The policy to apply to new accounts. This value must exist + * if the feature is enabled. The name must also match + * a policy that is defined in the serviceControlPolicy section. + */ + readonly scpPolicyName: string = 'QuarantineAccounts'; +} + +/** + * Service control policy configuration + */ +export abstract class ServiceControlPolicyConfig + implements t.TypeOf +{ + /** + * The friendly name to assign to the policy. + * The regex pattern that is used to validate this parameter is a string of any of the characters in the ASCII character range. + */ + readonly name: string = ''; + /** + * An optional description to assign to the policy. + */ + readonly description: string = ''; + /** + * Service control definition json file. This file must be present in config repository + */ + readonly policy: string = ''; + /** + * Kind of service control policy + */ + readonly type: string = 'customerManaged'; + /** + * Service control policy deployment targets + */ + readonly deploymentTargets: t.DeploymentTargets = new t.DeploymentTargets(); +} + +/** + * Organizations tag policy. + * + * Tag policies help you standardize tags on all tagged resources across your organization. + * You can use tag policies to define tag keys (including how they should be capitalized) and their allowed values. + */ +export abstract class TaggingPolicyConfig implements t.TypeOf { + /** + * The friendly name to assign to the policy. + * The regex pattern that is used to validate this parameter is a string of any of the characters in the ASCII character range. + */ + readonly name: string = ''; + /** + * An optional description to assign to the policy. + */ + readonly description: string = ''; + /** + * Tagging policy definition json file. This file must be present in config repository + */ + readonly policy: string = ''; + /** + * Tagging policy deployment targets + */ + readonly deploymentTargets: t.DeploymentTargets = new t.DeploymentTargets(); +} + +/** + * Organization backup policy + * + * Backup policies enable you to deploy organization-wide backup plans to help ensure compliance across your organization's accounts. + * Using policies helps ensure consistency in how you implement your backup plans + */ +export abstract class BackupPolicyConfig implements t.TypeOf { + /** + * The friendly name to assign to the policy. + * The regex pattern that is used to validate this parameter is a string of any of the characters in the ASCII character range. + */ + readonly name: string = ''; + readonly description: string = ''; + /** + * An optional description to assign to the policy. + */ + readonly policy: string = ''; + /** + * Backup policy deployment targets + */ + readonly deploymentTargets: t.DeploymentTargets = new t.DeploymentTargets(); +} + +/** + * Organization configuration + */ +export class OrganizationConfig implements t.TypeOf { + /** + * A name for the organization config file in config repository + * + * @default organization-config.yaml + */ + static readonly FILENAME = 'organization-config.yaml'; + + /** + * Indicates whether AWS Organization enabled. + * + */ + readonly enable = true; + + /** + * A Record of Organizational Unit configurations + * + * @see OrganizationalUnitConfig + * + * To create Security and Infrastructure OU in root , you need to provide following values for this parameter. + * Nested OU's start at root and configure all of the ou's in the path + * + * @example + * ``` + * organizationalUnits: + * - name: Security + * - name: Infrastructure + * - name: Sandbox + * - name: Sandbox/Pipeline + * - name: Sandbox/Development + * - name: Sandbox/Development/Application1 + * ``` + */ + readonly organizationalUnits: OrganizationalUnitConfig[] = [ + { + name: 'Security', + }, + { + name: 'Infrastructure', + }, + ]; + + /** + * Optionally provide a list of Organizational Unit IDs to bypass the usage of the + * AWS Organizations Client lookup. This is not a readonly member since we + * will initialize it with values if it is not provided + */ + public organizationalUnitIds: OrganizationalUnitIdConfig[] | undefined = undefined; + + /** + * A record of Quarantine New Accounts configuration + * @see QuarantineNewAccountsConfig + */ + readonly quarantineNewAccounts: QuarantineNewAccountsConfig | undefined = undefined; + + /** + * A Record of Service Control Policy configurations + * + * @see ServiceControlPolicyConfig + * + * To create service control policy named DenyDeleteVpcFlowLogs from service-control-policies/deny-delete-vpc-flow-logs.json file in config repository, you need to provide following values for this parameter. + * + * @example + * ``` + * serviceControlPolicies: + * - name: DenyDeleteVpcFlowLogs + * description: > + * This SCP prevents users or roles in any affected account from deleting + * Amazon Elastic Compute Cloud (Amazon EC2) flow logs or CloudWatch log + * groups or log streams. + * policy: service-control-policies/deny-delete-vpc-flow-logs.json + * type: customerManaged + * deploymentTargets: + * organizationalUnits: + * - Security + * ``` + */ + readonly serviceControlPolicies: ServiceControlPolicyConfig[] = []; + + /** + * A Record of Tagging Policy configurations + * + * @see TaggingPolicyConfig + * + * To create tagging policy named TagPolicy from tagging-policies/org-tag-policy.json file in config repository, you need to provide following values for this parameter. + * + * @example + * ``` + * taggingPolicies: + * - name: TagPolicy + * description: Organization Tagging Policy + * policy: tagging-policies/org-tag-policy.json + * deploymentTargets: + * organizationalUnits: + * - Root + * ``` + */ + readonly taggingPolicies: TaggingPolicyConfig[] = []; + + /** + * A Record of Backup Policy configurations + * + * @see BackupPolicyConfig + * + * To create backup policy named BackupPolicy from backup-policies/org-backup-policies.json file in config repository, you need to provide following values for this parameter. + * + * @example + * ``` + * backupPolicies: + * - name: BackupPolicy + * description: Organization Backup Policy + * policy: backup-policies/org-backup-policies.json + * deploymentTargets: + * organizationalUnits: + * - Root + * ``` + */ + readonly backupPolicies: BackupPolicyConfig[] = []; + + /** + * + * @param values + * @param configDir + */ + constructor(values?: t.TypeOf, configDir?: string) { + // + // Validation errors + // + const errors: string[] = []; + if (values) { + if (configDir) { + for (const serviceControlPolicy of values.serviceControlPolicies ?? []) { + // Validate presence of service control policy file + if (!fs.existsSync(path.join(configDir, serviceControlPolicy.policy))) { + errors.push( + `Invalid policy file ${serviceControlPolicy.policy} for service control policy ${serviceControlPolicy.name} !!!`, + ); + } + } + + // Validate presence of tagging policy file + for (const taggingPolicy of values.taggingPolicies ?? []) { + if (!fs.existsSync(path.join(configDir, taggingPolicy.policy))) { + errors.push(`Invalid policy file ${taggingPolicy.policy} for tagging policy ${taggingPolicy.name} !!!`); + } + } + + // Validate presence of backup policy file + for (const backupPolicy of values.backupPolicies ?? []) { + if (!fs.existsSync(path.join(configDir, backupPolicy.policy))) { + errors.push(`Invalid policy file ${backupPolicy.policy} for backup policy ${backupPolicy.name} !!!`); + } + } + } + + if (errors.length) { + throw new Error(`${OrganizationConfig.FILENAME} has ${errors.length} issues: ${errors.join(' ')}`); + } + Object.assign(this, values); + } + } + + /** + * Load from config file content + * @param dir + * @returns + */ + static load(dir: string): OrganizationConfig { + const buffer = fs.readFileSync(path.join(dir, OrganizationConfig.FILENAME), 'utf8'); + const values = t.parse(OrganizationConfigTypes.organizationConfig, yaml.load(buffer)); + return new OrganizationConfig(values, dir); + } + + /** + * Load from string content + * @param partition + */ + public async loadOrganizationalUnitIds(partition: string): Promise { + if (!this.enable) { + // do nothing + } else { + this.organizationalUnitIds = []; + } + if (this.organizationalUnitIds?.length == 0) { + let organizationsClient: AWS.Organizations; + if (partition === 'aws-us-gov') { + organizationsClient = new AWS.Organizations({ region: 'us-gov-west-1' }); + } else { + organizationsClient = new AWS.Organizations({ region: 'us-east-1' }); + } + + let rootId = ''; + + let nextToken: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => organizationsClient.listRoots({ NextToken: nextToken }).promise()); + for (const root of page.Roots ?? []) { + if (root.Name === 'Root' && root.Id && root.Arn) { + this.organizationalUnitIds?.push({ name: root.Name, id: root.Id, arn: root.Arn }); + rootId = root.Id; + } + } + nextToken = page.NextToken; + } while (nextToken); + + for (const item of this.organizationalUnits) { + let parentId = rootId; + let parentName = ''; + + const parentPath = this.getPath(item.name); + for (const parent of parentPath.split('/')) { + if (parent) { + let nextToken: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => + organizationsClient + .listOrganizationalUnitsForParent({ ParentId: parentId, NextToken: nextToken }) + .promise(), + ); + for (const ou of page.OrganizationalUnits ?? []) { + if (ou.Name === parent && ou.Id) { + parentId = ou.Id; + parentName = ou.Name; + } + } + nextToken = page.NextToken; + } while (nextToken); + } + } + + let nextToken: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => + organizationsClient + .listOrganizationalUnitsForParent({ ParentId: parentId, NextToken: nextToken }) + .promise(), + ); + for (const ou of page.OrganizationalUnits ?? []) { + const ouName = this.getOuName(item.name); + const ouParent = this.getParentOuName(item.name); + if (ou.Name === ouName && ouParent === parentName && ou.Id && ou.Arn) { + this.organizationalUnitIds?.push({ name: item.name, id: ou.Id, arn: ou.Arn }); + } + } + nextToken = page.NextToken; + } while (nextToken); + } + } + } + + public getOrganizationalUnitId(name: string): string { + if (!this.enable) { + // do nothing + } else { + const ou = this.organizationalUnitIds?.find(item => item.name === name); + if (ou) { + return ou.id; + } + } + throw new Error("Organizations not enabled or OU doesn't exist"); + } + + public getOrganizationalUnitArn(name: string): string { + if (!this.enable) { + // do nothing + } else { + const ou = this.organizationalUnitIds?.find(item => item.name === name); + if (ou) { + return ou.arn; + } + } + throw new Error("Organizations not enabled or OU doesn't exist"); + } + + public getPath(name: string): string { + //get the parent path + const pathIndex = name.lastIndexOf('/'); + const path = name.slice(0, pathIndex + 1).slice(0, -1); + if (path === '') { + return '/'; + } + return '/' + path; + } + + public getOuName(name: string): string { + const result = name.split('/').pop(); + if (result === undefined) { + return name; + } + return result; + } + + public getParentOuName(name: string): string { + const path = this.getPath(name); + const result = path.split('/').pop(); + if (result === undefined) { + return '/'; + } + return result; + } +} diff --git a/source/packages/@aws-accelerator/config/lib/security-config.ts b/source/packages/@aws-accelerator/config/lib/security-config.ts new file mode 100644 index 000000000..d8232c0e0 --- /dev/null +++ b/source/packages/@aws-accelerator/config/lib/security-config.ts @@ -0,0 +1,1404 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as t from './common-types'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as yaml from 'js-yaml'; + +/** + * AWS Accelerator SecurityConfig Types + */ +export class SecurityConfigTypes { + /** + * SNS notification subscription configuration. + */ + static readonly snsSubscriptionConfig = t.interface({ + level: t.nonEmptyString, + email: t.nonEmptyString, + }); + + /** + * Amazon Web Services S3 configuration + */ + static readonly s3PublicAccessBlockConfig = t.interface({ + /** + * S3 PublicAccessBlock enable flag + */ + enable: t.boolean, + /** + * List of AWS Account names to be excluded from configuring S3 PublicAccessBlock + */ + excludeAccounts: t.optional(t.array(t.string)), + }); + + /** + * AWS Macie configuration + */ + static readonly macieConfig = t.interface({ + /** + * Indicates whether AWS Macie enabled. + */ + enable: t.boolean, + /** + * List of AWS Region names to be excluded from configuring Amazon Macie + */ + excludeRegions: t.optional(t.array(t.region)), + /** + * Specifies how often to publish updates to policy findings for the account. This includes publishing updates to Security Hub and Amazon EventBridge (formerly called Amazon CloudWatch Events). + * An enum value that specifies how frequently findings are published + * Possible values FIFTEEN_MINUTES, ONE_HOUR, or SIX_HOURS + */ + policyFindingsPublishingFrequency: t.enums('FrequencyType', ['FIFTEEN_MINUTES', 'ONE_HOUR', 'SIX_HOURS']), + /** + * Specifies whether to publish sensitive data findings to Security Hub. If you set this value to true, Amazon Macie automatically publishes all sensitive data findings that weren't suppressed by a findings filter. The default value is false. + */ + publishSensitiveDataFindings: t.boolean, + /** + * Declaration of a (S3 Bucket) Lifecycle rule. + */ + lifecycleRules: t.optional(t.array(t.lifecycleRule)), + }); + + /** + * AWS GuardDuty S3 Protection configuration. + */ + static readonly guardDutyS3ProtectionConfig = t.interface({ + /** + * Indicates whether AWS GuardDuty S3 Protection enabled. + */ + enable: t.boolean, + /** + * List of AWS Region names to be excluded from configuring Amazon GuardDuty S3 Protection + */ + excludeRegions: t.optional(t.array(t.region)), + }); + + /** + * AWS GuardDuty Export Findings configuration. + */ + static readonly guardDutyExportFindingsConfig = t.interface({ + /** + * Indicates whether AWS GuardDuty Export Findings enabled. + */ + enable: t.boolean, + /** + * The type of resource for the publishing destination. Currently only Amazon S3 buckets are supported. + */ + destinationType: t.enums('DestinationType', ['S3']), + /** + * An enum value that specifies how frequently findings are exported, such as to CloudWatch Events. + * Possible values FIFTEEN_MINUTES, ONE_HOUR, or SIX_HOURS + */ + exportFrequency: t.enums('ExportFrequencyType', ['FIFTEEN_MINUTES', 'ONE_HOUR', 'SIX_HOURS']), + }); + + /** + * AWS GuardDuty configuration + */ + static readonly guardDutyConfig = t.interface({ + /** + * Indicates whether AWS GuardDuty enabled. + */ + enable: t.boolean, + /** + * List of AWS Region names to be excluded from configuring Amazon GuardDuty S3 Protection + */ + excludeRegions: t.optional(t.array(t.region)), + /** + * AWS GuardDuty Export Findings configuration. + */ + s3Protection: this.guardDutyS3ProtectionConfig, + /** + * AWS GuardDuty Export Findings configuration. + */ + exportConfiguration: this.guardDutyExportFindingsConfig, + /** + * Declaration of a (S3 Bucket) Life cycle rule. + */ + lifecycleRules: t.optional(t.array(t.lifecycleRule)), + }); + + /** + * AWS SecurityHub standards configuration + */ + static readonly securityHubStandardConfig = t.interface({ + /** + * An enum value that specifies one of three security standards supported by SecurityHub + * Possible values are 'AWS Foundational Security Best Practices v1.0.0', 'CIS AWS Foundations Benchmark v1.2.0' and 'PCI DSS v3.2.1' + */ + name: t.enums('ExportFrequencyType', [ + 'AWS Foundational Security Best Practices v1.0.0', + 'CIS AWS Foundations Benchmark v1.2.0', + 'PCI DSS v3.2.1', + ]), + /** + * Indicates whether given AWS SecurityHub standard enabled. + */ + enable: t.boolean, + /** + * An array of control names to be enabled for the given security standards + */ + controlsToDisable: t.optional(t.array(t.nonEmptyString)), + }); + + static readonly securityHubConfig = t.interface({ + enable: t.boolean, + excludeRegions: t.optional(t.array(t.region)), + standards: t.array(this.securityHubStandardConfig), + }); + + static readonly ebsDefaultVolumeEncryptionConfig = t.interface({ + enable: t.boolean, + excludeRegions: t.optional(t.array(t.region)), + }); + static readonly documentConfig = t.interface({ + name: t.nonEmptyString, + template: t.nonEmptyString, + }); + + static readonly documentSetConfig = t.interface({ + shareTargets: t.shareTargets, + documents: t.array(this.documentConfig), + }); + + static readonly ssmAutomationConfig = t.interface({ + excludeRegions: t.optional(t.array(t.region)), + documentSets: t.array(this.documentSetConfig), + }); + + /** + * Central security services configuration + */ + static readonly centralSecurityServicesConfig = t.interface({ + delegatedAdminAccount: t.nonEmptyString, + ebsDefaultVolumeEncryption: SecurityConfigTypes.ebsDefaultVolumeEncryptionConfig, + s3PublicAccessBlock: SecurityConfigTypes.s3PublicAccessBlockConfig, + macie: SecurityConfigTypes.macieConfig, + guardduty: SecurityConfigTypes.guardDutyConfig, + securityHub: SecurityConfigTypes.securityHubConfig, + ssmAutomation: this.ssmAutomationConfig, + }); + + static readonly accessAnalyzerConfig = t.interface({ + enable: t.boolean, + }); + + static readonly iamPasswordPolicyConfig = t.interface({ + allowUsersToChangePassword: t.boolean, + hardExpiry: t.boolean, + requireUppercaseCharacters: t.boolean, + requireLowercaseCharacters: t.boolean, + requireSymbols: t.boolean, + requireNumbers: t.boolean, + minimumPasswordLength: t.number, + passwordReusePrevention: t.number, + maxPasswordAge: t.number, + }); + + static readonly customRuleLambdaType = t.interface({ + sourceFilePath: t.nonEmptyString, + handler: t.nonEmptyString, + runtime: t.nonEmptyString, + rolePolicyFile: t.nonEmptyString, + }); + + static readonly triggeringResourceType = t.interface({ + lookupType: t.enums('ResourceLookupType', ['ResourceId', 'Tag', 'ResourceTypes']), + lookupKey: t.nonEmptyString, + lookupValue: t.array(t.nonEmptyString), + }); + + static readonly customRuleConfigType = t.interface({ + lambda: this.customRuleLambdaType, + periodic: t.optional(t.boolean), + maximumExecutionFrequency: t.enums('ExecutionFrequency', [ + 'One_Hour', + 'Three_Hours', + 'Six_Hours', + 'Twelve_Hours', + 'TwentyFour_Hours', + ]), + configurationChanges: t.optional(t.boolean), + triggeringResources: this.triggeringResourceType, + }); + + /** + * Config rule remediation input parameter configuration type + */ + static readonly remediationParametersConfigType = t.interface({ + /** + * Name of the parameter + */ + name: t.nonEmptyString, + /** + * Parameter value + */ + value: t.nonEmptyString, + /** + * Data type of the parameter, allowed value (StringList or String) + */ + type: t.enums('ParameterDataType', ['String', 'StringList']), + }); + + static readonly configRuleRemediationType = t.interface({ + /** + * SSM document execution role policy definition file + */ + rolePolicyFile: t.nonEmptyString, + /** + * The remediation is triggered automatically. + */ + automatic: t.boolean, + /** + * Target ID is the name of the public or shared SSM document. + */ + targetId: t.nonEmptyString, + /** + * Owner account name for the target SSM document, if not provided audit account ID will be used + */ + targetAccountName: t.optional(t.nonEmptyString), + /** + * Version of the target. For example, version of the SSM document. + */ + targetVersion: t.optional(t.nonEmptyString), + /** + * Optional target SSM document lambda function details. This is required when remediation SSM document uses action as aws:invokeLambdaFunction for remediation + */ + targetDocumentLambda: t.optional(SecurityConfigTypes.customRuleLambdaType), + /** + * Maximum time in seconds that AWS Config runs auto-remediation. If you do not select a number, the default is 60 seconds. + */ + retryAttemptSeconds: t.optional(t.number), + /** + * The maximum number of failed attempts for auto-remediation. If you do not select a number, the default is 5. + */ + maximumAutomaticAttempts: t.optional(t.number), + /** + * An object of the RemediationParameterValue. + */ + // parameters: t.optional(t.dictionary(t.nonEmptyString, t.nonEmptyString)), + parameters: t.optional(t.array(SecurityConfigTypes.remediationParametersConfigType)), + }); + + static readonly configRule = t.interface({ + name: t.nonEmptyString, + description: t.optional(t.nonEmptyString), + identifier: t.optional(t.nonEmptyString), + inputParameters: t.optional(t.dictionary(t.nonEmptyString, t.nonEmptyString)), + complianceResourceTypes: t.optional(t.array(t.nonEmptyString)), + type: t.optional(t.nonEmptyString), + customRule: t.optional(this.customRuleConfigType), + remediation: t.optional(this.configRuleRemediationType), + }); + + static readonly awsConfigRuleSet = t.interface({ + deploymentTargets: t.deploymentTargets, + rules: t.array(this.configRule), + }); + + static readonly awsConfig = t.interface({ + enableConfigurationRecorder: t.boolean, + enableDeliveryChannel: t.boolean, + ruleSets: t.array(this.awsConfigRuleSet), + }); + + static readonly metricConfig = t.interface({ + filterName: t.nonEmptyString, + logGroupName: t.nonEmptyString, + filterPattern: t.nonEmptyString, + metricNamespace: t.nonEmptyString, + metricName: t.nonEmptyString, + metricValue: t.nonEmptyString, + }); + + static readonly metricSetConfig = t.interface({ + regions: t.optional(t.array(t.nonEmptyString)), + deploymentTargets: t.deploymentTargets, + metrics: t.array(this.metricConfig), + }); + + static readonly alarmConfig = t.interface({ + alarmName: t.nonEmptyString, + alarmDescription: t.nonEmptyString, + snsAlertLevel: t.nonEmptyString, + metricName: t.nonEmptyString, + namespace: t.nonEmptyString, + comparisonOperator: t.nonEmptyString, + evaluationPeriods: t.number, + period: t.number, + statistic: t.nonEmptyString, + threshold: t.number, + treatMissingData: t.nonEmptyString, + }); + + static readonly alarmSetConfig = t.interface({ + regions: t.optional(t.array(t.nonEmptyString)), + deploymentTargets: t.deploymentTargets, + alarms: t.array(this.alarmConfig), + }); + + static readonly cloudWatchConfig = t.interface({ + metricSets: t.array(this.metricSetConfig), + alarmSets: t.array(this.alarmSetConfig), + }); + + static readonly securityConfig = t.interface({ + centralSecurityServices: this.centralSecurityServicesConfig, + accessAnalyzer: this.accessAnalyzerConfig, + iamPasswordPolicy: this.iamPasswordPolicyConfig, + awsConfig: this.awsConfig, + cloudWatch: this.cloudWatchConfig, + }); +} + +/** + * AWS S3 block public access configuration + */ +export class S3PublicAccessBlockConfig implements t.TypeOf { + /** + * Indicates whether AWS S3 block public access enabled. + */ + readonly enable = false; + /** + * List of AWS Region names to be excluded from configuring block S3 public access + */ + readonly excludeAccounts: string[] = []; +} + +/** + * Amazon Macie Configuration + */ +export class MacieConfig implements t.TypeOf { + /** + * Indicates whether AWS Macie enabled. + */ + readonly enable = false; + /** + * List of AWS Region names to be excluded from configuring Amazon Macie + */ + readonly excludeRegions: t.Region[] = []; + /** + * Specifies how often to publish updates to policy findings for the account. This includes publishing updates to Security Hub and Amazon EventBridge (formerly called Amazon CloudWatch Events). + * An enum value that specifies how frequently findings are published + * Possible values FIFTEEN_MINUTES, ONE_HOUR, or SIX_HOURS + */ + readonly policyFindingsPublishingFrequency = 'FIFTEEN_MINUTES'; + /** + * Specifies whether to publish sensitive data findings to Security Hub. If you set this value to true, Amazon Macie automatically publishes all sensitive data findings that weren't suppressed by a findings filter. The default value is false. + */ + readonly publishSensitiveDataFindings = true; + /** + * Declaration of a (S3 Bucket) Life cycle rule. + */ + readonly lifecycleRules: t.LifecycleRule[] | undefined = undefined; +} + +/** + * AWS GuardDuty S3 Protection configuration. + */ +export class GuardDutyS3ProtectionConfig implements t.TypeOf { + /** + * Indicates whether AWS GuardDuty S3 Protection enabled. + */ + readonly enable = false; + /** + * List of AWS Region names to be excluded from configuring Amazon GuardDuty S3 Protection + */ + readonly excludeRegions: t.Region[] = []; +} + +/** + * AWS GuardDuty Export Findings configuration. + */ +export class GuardDutyExportFindingsConfig + implements t.TypeOf +{ + /** + * Indicates whether AWS GuardDuty Export Findings enabled. + */ + readonly enable = false; + /** + * The type of resource for the publishing destination. Currently only Amazon S3 buckets are supported. + */ + readonly destinationType = 'S3'; + /** + * An enum value that specifies how frequently findings are exported, such as to CloudWatch Events. + * Possible values FIFTEEN_MINUTES, ONE_HOUR, or SIX_HOURS + */ + readonly exportFrequency = 'FIFTEEN_MINUTES'; +} + +/** + * AWS GuardDuty configuration + */ +export class GuardDutyConfig implements t.TypeOf { + /** + * Indicates whether AWS GuardDuty enabled. + */ + readonly enable = false; + /** + * List of AWS Region names to be excluded from configuring Amazon GuardDuty + */ + readonly excludeRegions: t.Region[] = []; + /** + * AWS GuardDuty S3 Protection configuration. + * @type object + */ + readonly s3Protection: GuardDutyS3ProtectionConfig = new GuardDutyS3ProtectionConfig(); + /** + * AWS GuardDuty Export Findings configuration. + * @type object + */ + readonly exportConfiguration: GuardDutyExportFindingsConfig = new GuardDutyExportFindingsConfig(); + /** + * Declaration of a (S3 Bucket) Life cycle rule. + */ + readonly lifecycleRules: t.LifecycleRule[] | undefined = undefined; +} + +/** + * AWS SecurityHub standards configuration + */ +export class SecurityHubStandardConfig implements t.TypeOf { + /** + * An enum value that specifies one of three security standards supported by SecurityHub + * Possible values are 'AWS Foundational Security Best Practices v1.0.0', 'CIS AWS Foundations Benchmark v1.2.0' and 'PCI DSS v3.2.1' + */ + readonly name = ''; + /** + * Indicates whether given AWS SecurityHub standard enabled. + */ + readonly enable = true; + /** + * An array of control names to be enabled for the given security standards + */ + readonly controlsToDisable: string[] = []; +} + +/** + * AWS SecurityHub configuration + */ +export class SecurityHubConfig implements t.TypeOf { + /** + * Indicates whether AWS SecurityHub enabled. + */ + readonly enable = false; + /** + * List of AWS Region names to be excluded from configuring SecurityHub + */ + readonly excludeRegions: t.Region[] = []; + /** + * SecurityHub standards configuration + */ + readonly standards: SecurityHubStandardConfig[] = []; +} + +/** + * AWS SNS Notification subscription configuration + */ +export class SnsSubscriptionConfig implements t.TypeOf { + /** + * Notification level high, medium or low + */ + readonly level: string = ''; + /** + * Subscribing email address + */ + readonly email: string = ''; +} + +/** + * AWS EBS default encryption configuration + */ +export class ebsDefaultVolumeEncryptionConfig + implements t.TypeOf +{ + /** + * Indicates whether AWS EBS volume have default encryption enabled. + */ + readonly enable = false; + /** + * List of AWS Region names to be excluded from configuring AWS EBS volume default encryption + */ + readonly excludeRegions: t.Region[] = []; +} + +/** + * AWS Systems Manager document configuration + */ +export class DocumentConfig implements t.TypeOf { + /** + * Name of document to be created + */ + readonly name: string = ''; + /** + * Document template file path. This file must be available in accelerator config repository. + */ + readonly template: string = ''; +} + +/** + * AWS Systems Manager document sharing configuration + */ +export class DocumentSetConfig implements t.TypeOf { + /** + * Document share target, valid value should be any organizational unit. + * Document will be shared with every account within the given OU + */ + readonly shareTargets: t.ShareTargets = new t.ShareTargets(); + /** + * List of the documents to be shared + */ + readonly documents: DocumentConfig[] = []; +} + +/** + * AWS Systems Manager automation configuration + */ +export class SsmAutomationConfig implements t.TypeOf { + /** + * List of AWS Region names to be excluded from configuring block S3 public access + */ + readonly excludeRegions: t.Region[] = []; + /** + * List of documents for automation + */ + readonly documentSets: DocumentSetConfig[] = []; +} + +/** + * AWS Accelerator central security services configuration + */ +export class CentralSecurityServicesConfig + implements t.TypeOf +{ + /** + * Designated administrator account name for accelerator security services. + * AWS organizations designate a member account as a delegated administrator for the + * organization users and roles from that account can perform administrative actions for security services like + * Macie, GuardDuty and SecurityHub. Without designated administrator account administrative tasks for + * security services are performed only by users or roles in the organization's management account. + * This helps you to separate management of the organization from management of these security services. + * Accelerator use Audit account as designated administrator account. + * @type string + * @default Audit + * + * To make Audit account as designated administrator account for every security services configured by accelerator, you need to provide below value for this parameter + * @example + * ``` + * delegatedAdminAccount: Audit + * ``` + */ + readonly delegatedAdminAccount = 'Audit'; + /** + * AWS Elastic Block Store default encryption configuration + * + * Accelerator use this parameter to configure EBS default encryption. + * Accelerator will create KMS key for every AWS environment (account and region), which will be used as default EBS encryption key. + * + * To enable EBS default encryption in every region accelerator implemented, you need to provide below value for this parameter. + * + * @example + * ``` + * ebsDefaultVolumeEncryption: + * enable: true + * excludeRegions: [] + * ``` + */ + readonly ebsDefaultVolumeEncryption: ebsDefaultVolumeEncryptionConfig = new ebsDefaultVolumeEncryptionConfig(); + /** + * AWS S3 public access block configuration + * + * Accelerator use this parameter to block AWS S3 public access + * + * To enable S3 public access blocking in every region accelerator implemented, you need to provide below value for this parameter. + * + * @example + * ``` + * s3PublicAccessBlock: + * enable: true + * excludeAccounts: [] + * ``` + */ + readonly s3PublicAccessBlock: S3PublicAccessBlockConfig = new S3PublicAccessBlockConfig(); + /** + * AWS SNS subscription configuration + * + * Accelerator use this parameter to define AWS SNS notification configuration. + * + * To enable high, medium and low SNS notifications, you need to provide below value for this parameter. + * @example + * ``` + * snsSubscriptions: + * - level: High + * email: @example.com + * - level: Medium + * email: @example.com + * - level: Low + * email: @example.com + * ``` + */ + readonly snsSubscriptions: SnsSubscriptionConfig[] = []; + /** + * Amazon Macie Configuration + * + * Accelerator use this parameter to define AWS Macie configuration. + * + * To enable Macie in every region accelerator implemented and + * set fifteen minutes of frequency to publish updates to policy findings for the account with + * publishing sensitive data findings to Security Hub. + * you need to provide below value for this parameter. + * @example + * ``` + * macie: + * enable: true + * excludeRegions: [] + * policyFindingsPublishingFrequency: FIFTEEN_MINUTES + * publishSensitiveDataFindings: true + * ``` + */ + readonly macie: MacieConfig = new MacieConfig(); + /** + * Amazon GuardDuty Configuration + */ + readonly guardduty: GuardDutyConfig = new GuardDutyConfig(); + /** + * AWS SecurityHub configuration + * + * Accelerator use this parameter to define AWS SecurityHub configuration. + * + * To enable AWS SecurityHub for all regions and + * enable "AWS Foundational Security Best Practices v1.0.0" security standard for IAM.1 & EC2.10 controls + * you need provide below value for this parameter. + * + * @example + * ``` + * securityHub: + * enable: true + * regionAggregation: true + * excludeRegions: [] + * standards: + * - name: AWS Foundational Security Best Practices v1.0.0 + * enable: true + * controlsToDisable: + * - IAM.1 + * - EC2.10 + * ``` + */ + readonly securityHub: SecurityHubConfig = new SecurityHubConfig(); + /** + * AWS Systems Manager Document configuration + * + * Accelerator use this parameter to define AWS Systems Manager documents configuration. + * SSM documents are created in designated administrator account for security services, i.e. Audit account. + * + * To create a SSM document named as "SSM-ELB-Enable-Logging" in every region accelerator implemented and share this + * document with Root organizational unit(OU), you need to provide below value for this parameter. + * To share document to specific account uncomment accounts list. A valid SSM document template file ssm-documents/ssm-elb-enable-logging.yaml + * must be present in Accelerator config repository. Accelerator will use this template file to create the document. + * + * @example + * ``` + * ssmAutomation: + * excludeRegions: [] + * documentSets: + * - shareTargets: + * organizationalUnits: + * - Root + * # accounts: + * # - Network + * documents: + * - name: SSM-ELB-Enable-Logging + * template: ssm-documents/ssm-elb-enable-logging.yaml + * ``` + */ + readonly ssmAutomation: SsmAutomationConfig = new SsmAutomationConfig(); +} + +/** + * AWS AccessAnalyzer configuration + */ +export class AccessAnalyzerConfig implements t.TypeOf { + /** + * Indicates whether AWS AccessAnalyzer enabled in your organization. + * + * Once enabled, IAM Access Analyzer analyzes policies and reports a list of findings for resources that grant public or cross-account access from outside your AWS Organizations in the IAM console and through APIs. + */ + readonly enable = false; +} + +/** + * IAM password policy configuration + */ +export class IamPasswordPolicyConfig implements t.TypeOf { + /** + * Allows all IAM users in your account to use the AWS Management Console to change their own passwords. + * + * @default true + */ + readonly allowUsersToChangePassword = true; + /** + * Prevents IAM users who are accessing the account via the AWS Management Console from setting a new console password after their password has expired. + * The IAM user cannot access the console until an administrator resets the password. + * + * @default true + */ + readonly hardExpiry = false; + /** + * Specifies whether IAM user passwords must contain at least one uppercase character from the ISO basic Latin alphabet (A to Z). + * + * If you do not specify a value for this parameter, then the operation uses the default value of false. The result is that passwords do not require at least one uppercase character. + * + * @default true + */ + readonly requireUppercaseCharacters = true; + /** + * Specifies whether IAM user passwords must contain at least one lowercase character from the ISO basic Latin alphabet (a to z). + * + * If you do not specify a value for this parameter, then the operation uses the default value of false. The result is that passwords do not require at least one lowercase character. + * + * @default true + */ + readonly requireLowercaseCharacters = true; + /** + * Specifies whether IAM user passwords must contain at least one of the following non-alphanumeric characters: + * + * ! @ # $ % ^ & * ( ) _ + - = [ ] { } | ' + * + * If you do not specify a value for this parameter, then the operation uses the default value of false. The result is that passwords do not require at least one symbol character. + * + * @default true + */ + readonly requireSymbols = true; + /** + * Specifies whether IAM user passwords must contain at least one numeric character (0 to 9). + * + * If you do not specify a value for this parameter, then the operation uses the default value of false. The result is that passwords do not require at least one numeric character. + * + * @default true + */ + readonly requireNumbers = true; + /** + * The minimum number of characters allowed in an IAM user password. + * + * If you do not specify a value for this parameter, then the operation uses the default value of 6. + * + * @default 14 + */ + readonly minimumPasswordLength = 14; + /** + * Specifies the number of previous passwords that IAM users are prevented from reusing. + * + * If you do not specify a value for this parameter, then the operation uses the default value of 0. + * The result is that IAM users are not prevented from reusing previous passwords. + * + * @default 24 + */ + readonly passwordReusePrevention = 24; + /** + * The number of days that an IAM user password is valid. + * + * If you do not specify a value for this parameter, then the operation uses the default value of 0. The result is that IAM user passwords never expire. + * + * @default 90 + */ + readonly maxPasswordAge = 90; +} + +/** + * AWS ConfigRule configuration + */ +export class ConfigRule implements t.TypeOf { + /** + * A name for the AWS Config rule. + */ + readonly name = ''; + /** + * A description about this AWS Config rule. + */ + readonly description = ''; + /** + * The identifier of the AWS managed rule. + */ + readonly identifier = ''; + /** + * Input parameter values that are passed to the AWS Config rule. + */ + readonly inputParameters = {}; + /** + * Defines which resources trigger an evaluation for an AWS Config rule. + */ + readonly complianceResourceTypes: string[] = []; + /** + * Config rule type Managed or Custom. For custom config rule, this parameter value is Custom, when creating managed config rule this parameter value can be undefined or empty string + */ + readonly type = ''; + /** + * A custom config rule is backed by AWS Lambda function. This is required when creating custom config rule. + */ + readonly customRule = { + /** + * The Lambda function to run. + */ + lambda: { + /** + * The source code file path of your Lambda function. This is a zip file containing lambda function, this file must be available in config repository. + */ + sourceFilePath: '', + /** + * The name of the method within your code that Lambda calls to execute your function. The format includes the file name. It can also include namespaces and other qualifiers, depending on the runtime. + * For more information, see https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-features.html#gettingstarted-features-programmingmodel. + */ + handler: '', + /** + * The runtime environment for the Lambda function that you are uploading. For valid values, see the Runtime property in the AWS Lambda Developer Guide. + */ + runtime: '', + /** + * Lambda execution role policy definition file + */ + rolePolicyFile: '', + }, + /** + * Whether to run the rule on a fixed frequency. + * + * @default true + */ + periodic: true, + /** + * The maximum frequency at which the AWS Config rule runs evaluations. + * + * Default: + * MaximumExecutionFrequency.TWENTY_FOUR_HOURS + */ + maximumExecutionFrequency: 'TwentyFour_Hours', + /** + * Whether to run the rule on configuration changes. + * + * Default: + * false + */ + configurationChanges: true, + /** + * Defines which resources trigger an evaluation for an AWS Config rule. + */ + triggeringResources: { + /** + * An enum to identify triggering resource types. + * Possible values ResourceId, Tag, or ResourceTypes + * + * Triggering resource can be lookup by resource id, tags or resource types. + */ + lookupType: '', + /** + * Resource lookup type, resource can be lookup by tag or types. When resource needs to lookup by tag, this field will have tag name. + */ + lookupKey: '', + /** + * Resource lookup value, when resource lookup using tag, this field will have tag value to search resource. + */ + lookupValue: [], + }, + }; + /** + * A remediation for the config rule, auto remediation to automatically remediate noncompliant resources. + */ + readonly remediation = { + /** + * Remediation assume role policy definition json file. This file must be present in config repository. + * + * Create your own custom remediation actions using AWS Systems Manager Automation documents. + * When a role needed to be created to perform custom remediation actions, role permission needs to be defined in this file. + */ + rolePolicyFile: '', + /** + * The remediation is triggered automatically. + */ + automatic: true, + /** + * Target ID is the name of the public document. + * + * The name of the AWS SSM document to perform custom remediation actions. + */ + targetId: '', + /** + * Name of the account owning the public document to perform custom remediation actions. + * Accelerator creates these documents in Audit account and shared with other accounts. + */ + targetAccountName: '', + /** + * Version of the target. For example, version of the SSM document. + * + * If you make backward incompatible changes to the SSM document, you must call PutRemediationConfiguration API again to ensure the remediations can run. + */ + targetVersion: '', + /** + * Target SSM document remediation lambda function + */ + targetDocumentLambda: { + /** + * The source code file path of your Lambda function. This is a zip file containing lambda function, this file must be available in config repository. + */ + sourceFilePath: '', + /** + * The name of the method within your code that Lambda calls to execute your function. The format includes the file name. It can also include namespaces and other qualifiers, depending on the runtime. + * For more information, see https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-features.html#gettingstarted-features-programmingmodel. + */ + handler: '', + /** + * The runtime environment for the Lambda function that you are uploading. For valid values, see the Runtime property in the AWS Lambda Developer Guide. + */ + runtime: '', + /** + * Lambda execution role policy definition file + */ + rolePolicyFile: '', + }, + /** + * Maximum time in seconds that AWS Config runs auto-remediation. If you do not select a number, the default is 60 seconds. + * + * For example, if you specify RetryAttemptSeconds as 50 seconds and MaximumAutomaticAttempts as 5, AWS Config will run auto-remediations 5 times within 50 seconds before throwing an exception. + */ + retryAttemptSeconds: 0, + /** + * The maximum number of failed attempts for auto-remediation. If you do not select a number, the default is 5. + * + * For example, if you specify MaximumAutomaticAttempts as 5 with RetryAttemptSeconds as 50 seconds, AWS Config will put a RemediationException on your behalf for the failing resource after the 5th failed attempt within 50 seconds. + */ + maximumAutomaticAttempts: 0, + /** + * List of remediation parameters + * + */ + parameters: [], + }; +} + +/** + * List of AWS Config rules + */ +export class AwsConfigRuleSet implements t.TypeOf { + /** + * Config ruleset deployment target. + * + * To configure AWS Config rules into Root and Infrastructure organizational units, you need to provide below value for this parameter. + * + * @example + * ``` + * - deploymentTargets: + * organizationalUnits: + * - Root + * - Infrastructure + * ``` + */ + readonly deploymentTargets: t.DeploymentTargets = new t.DeploymentTargets(); + /** + * AWS Config ruleset + * + * Following example will create a custom rule named accelerator-attatch-ec2-instance-profile with remediation + * and a managed rule named accelerator-iam-user-group-membership-check without remediation + * + * @example + * ``` + * rules: + * - name: accelerator-attatch-ec2-instance-profile + * type: Custom + * description: Custom role to remediate ec2 instance profile to EC2 instances + * inputParameters: + * customRule: + * lambda: + * sourceFilePath: custom-config-rules/attach-ec2-instance-profile.zip + * handler: index.handler + * runtime: nodejs14.x + * periodic: true + * maximumExecutionFrequency: Six_Hours + * configurationChanges: true + * triggeringResources: + * lookupType: ResourceTypes + * lookupKey: ResourceTypes + * lookupValue: + * - AWS::EC2::Instance + * - name: accelerator-iam-user-group-membership-check + * complianceResourceTypes: + * - AWS::IAM::User + * identifier: IAM_USER_GROUP_MEMBERSHIP_CHECK + * ``` + */ + readonly rules: ConfigRule[] = []; +} + +/** + * AWS Config rule + */ +export class AwsConfig implements t.TypeOf { + /** + * Indicates whether AWS Config recorder enabled. + * + * To enable AWS Config, you must create a configuration recorder and a delivery channel. + * + * ConfigurationRecorder resource describes the AWS resource types for which AWS Config records configuration changes. The configuration recorder stores the configurations of the supported resources in your account as configuration items. + */ + readonly enableConfigurationRecorder = true; + /** + * Indicates whether delivery channel enabled. + * + * AWS Config uses the delivery channel to deliver the configuration changes to your Amazon S3 bucket or Amazon SNS topic. + */ + readonly enableDeliveryChannel = true; + /** + * AWS Config rule sets + */ + readonly ruleSets: AwsConfigRuleSet[] = []; +} + +/** + * AWS CloudWatch Metric configuration + */ +export class MetricConfig implements t.TypeOf { + /** + * Metric filter name + */ + readonly filterName: string = ''; + /** + * The log group to create the filter on. + */ + readonly logGroupName: string = ''; + /** + * Pattern to search for log events. + */ + readonly filterPattern: string = ''; + /** + * The namespace of the metric to emit. + */ + readonly metricNamespace: string = ''; + /** + * The name of the metric to emit. + */ + readonly metricName: string = ''; + /** + * The value to emit for the metric. + * + * Can either be a literal number (typically “1”), or the name of a field in the structure to take the value from the matched event. If you are using a field value, the field value must have been matched using the pattern. + * + * If you want to specify a field from a matched JSON structure, use '$.fieldName', and make sure the field is in the pattern (if only as '$.fieldName = *'). + * + * If you want to specify a field from a matched space-delimited structure, use '$fieldName'. + */ + readonly metricValue: string = ''; + /** + * Sets how this alarm is to handle missing data points. + */ + readonly treatMissingData: string | undefined = undefined; +} + +/** + * AWS CloudWatch Metric set configuration + */ +export class MetricSetConfig implements t.TypeOf { + /** + * AWS region names to configure CloudWatch Metrics + */ + readonly regions: string[] | undefined = undefined; + /** + * Deployment targets for CloudWatch Metrics configuration + */ + readonly deploymentTargets: t.DeploymentTargets = new t.DeploymentTargets(); + /** + * AWS CloudWatch Metric list + * + * Following example will create metric filter RootAccountMetricFilter for aws-controltower/CloudTrailLogs log group + * + * @example + * ``` + * metrics: + * # CIS 1.1 – Avoid the use of the "root" account + * - filterName: RootAccountMetricFilter + * logGroupName: aws-controltower/CloudTrailLogs + * filterPattern: '{$.userIdentity.type="Root" && $.userIdentity.invokedBy NOT EXISTS && $.eventType !="AwsServiceEvent"}' + * metricNamespace: LogMetrics + * metricName: RootAccount + * metricValue: "1" + * ``` + */ + readonly metrics: MetricConfig[] = []; +} + +/** + * AWS CloudWatch Alarm configuration + */ +export class AlarmConfig implements t.TypeOf { + /** + * Name of the alarm + */ + readonly alarmName: string = ''; + /** + * Description for the alarm + */ + readonly alarmDescription: string = ''; + /** + * Alert SNS notification level + */ + readonly snsAlertLevel: string = ''; + /** + * Name of the metric. + */ + readonly metricName: string = ''; + /** + * Namespace of the metric. + */ + readonly namespace: string = ''; + /** + * Comparison to use to check if metric is breaching + */ + readonly comparisonOperator: string = ''; + /** + * The number of periods over which data is compared to the specified threshold. + */ + readonly evaluationPeriods: number = 1; + /** + * The period over which the specified statistic is applied. + */ + readonly period: number = 300; + /** + * What functions to use for aggregating. + * + * Can be one of the following: + * - “Minimum” | “min” + * - “Maximum” | “max” + * - “Average” | “avg” + * - “Sum” | “sum” + * - “SampleCount | “n” + * - “pNN.NN” + */ + readonly statistic: string = ''; + /** + * The value against which the specified statistic is compared. + */ + readonly threshold: number = 1; + /** + * Sets how this alarm is to handle missing data points. + */ + readonly treatMissingData: string = ''; +} + +/** + * AWS CloudWatch Alarm sets + */ +export class AlarmSetConfig implements t.TypeOf { + /** + * AWS region names to configure CloudWatch Alarms + */ + readonly regions: string[] | undefined = undefined; + /** + * Deployment targets for CloudWatch Alarms configuration + */ + readonly deploymentTargets: t.DeploymentTargets = new t.DeploymentTargets(); + /** + * List of AWS CloudWatch Alarms + * + * Following example will create CIS-1.1-RootAccountUsage alarm for RootAccountUsage metric with notification level low + * + * @example + * ``` + * alarms: + * # CIS 1.1 – Avoid the use of the "root" account + * - alarmName: CIS-1.1-RootAccountUsage + * alarmDescription: Alarm for usage of "root" account + * snsAlertLevel: Low + * metricName: RootAccountUsage + * namespace: LogMetrics + * comparisonOperator: GreaterThanOrEqualToThreshold + * evaluationPeriods: 1 + * period: 300 + * statistic: Sum + * threshold: 1 + * treatMissingData: notBreaching + * ``` + */ + readonly alarms: AlarmConfig[] = []; +} + +/** + * AWS CloudWatch configuration + */ +export class CloudWatchConfig implements t.TypeOf { + /** + * List AWS CloudWatch Metrics configuration + * + * Following example will create metric filter RootAccountMetricFilter for aws-controltower/CloudTrailLogs log group + * + * @example + * ``` + * metrics: + * # CIS 1.1 – Avoid the use of the "root" account + * - filterName: RootAccountMetricFilter + * logGroupName: aws-controltower/CloudTrailLogs + * filterPattern: '{$.userIdentity.type="Root" && $.userIdentity.invokedBy NOT EXISTS && $.eventType !="AwsServiceEvent"}' + * metricNamespace: LogMetrics + * metricName: RootAccount + * metricValue: "1" + * ``` + */ + readonly metricSets: MetricSetConfig[] = []; + /** + * List AWS CloudWatch Alarms configuration + * + * Following example will create CIS-1.1-RootAccountUsage alarm for RootAccountUsage metric with notification level low + * + * @example + * ``` + * alarms: + * # CIS 1.1 – Avoid the use of the "root" account + * - alarmName: CIS-1.1-RootAccountUsage + * alarmDescription: Alarm for usage of "root" account + * snsAlertLevel: Low + * metricName: RootAccountUsage + * namespace: LogMetrics + * comparisonOperator: GreaterThanOrEqualToThreshold + * evaluationPeriods: 1 + * period: 300 + * statistic: Sum + * threshold: 1 + * treatMissingData: notBreaching + * ``` + */ + readonly alarmSets: AlarmSetConfig[] = []; +} + +/** + * Accelerator security configuration + */ +export class SecurityConfig implements t.TypeOf { + /** + * Security configuration file name, this file must be present in accelerator config repository + */ + static readonly FILENAME = 'security-config.yaml'; + + /** + * Central security configuration + */ + readonly centralSecurityServices: CentralSecurityServicesConfig = new CentralSecurityServicesConfig(); + readonly accessAnalyzer: AccessAnalyzerConfig = new AccessAnalyzerConfig(); + readonly iamPasswordPolicy: IamPasswordPolicyConfig = new IamPasswordPolicyConfig(); + readonly awsConfig: AwsConfig = new AwsConfig(); + readonly cloudWatch: CloudWatchConfig = new CloudWatchConfig(); + + constructor(values?: t.TypeOf, configDir?: string) { + // + // Validation errors + // + const errors: string[] = []; + + if (values) { + // + // SSM Document validations + + const ssmDocuments: { name: string; template: string }[] = []; + for (const documentSet of values.centralSecurityServices.ssmAutomation.documentSets) { + for (const document of documentSet.documents ?? []) { + ssmDocuments.push(document); + } + } + + // Validate presence of SSM document files + if (configDir) { + for (const ssmDocument of ssmDocuments) { + if (!fs.existsSync(path.join(configDir, ssmDocument.template))) { + errors.push(`SSM document ${ssmDocument.name} template file ${ssmDocument.template} not found !!!`); + } + } + + for (const ruleSet of values.awsConfig.ruleSets ?? []) { + for (const rule of ruleSet.rules) { + if (rule.type === 'Custom' && rule.customRule) { + // Validate presence of custom rule lambda function zip file + if (!fs.existsSync(path.join(configDir, rule.customRule.lambda.sourceFilePath))) { + errors.push( + `Custom rule: ${rule.name} lambda function file ${rule.customRule.lambda.sourceFilePath} not found`, + ); + } + // Validate presence of custom rule lambda function role policy file + if (!fs.existsSync(path.join(configDir, rule.customRule.lambda.rolePolicyFile))) { + errors.push( + `Custom rule: ${rule.name} lambda function role policy file ${rule.customRule.lambda.rolePolicyFile} not found`, + ); + } + } + if (rule.remediation) { + // Validate presence of rule remediation assume role definition file + if (!fs.existsSync(path.join(configDir, rule.remediation.rolePolicyFile))) { + errors.push( + `Rule: ${rule.name}, remediation assume role definition file ${rule.remediation.rolePolicyFile} not found`, + ); + } + // Validate presence of SSM document before used as remediation target + if (!ssmDocuments.find(item => item.name === rule.remediation?.targetId)) { + errors.push( + `Rule: ${rule.name}, remediation target SSM document ${rule.remediation?.targetId} not found in ssm automation document lists`, + ); + // Validate presence of custom rule's remediation SSMS document invoke lambda function zip file + if (rule.remediation.targetDocumentLambda) { + if (!fs.existsSync(path.join(configDir, rule.remediation.targetDocumentLambda.sourceFilePath))) { + errors.push( + `Rule: ${rule.name}, remediation target SSM document lambda function file ${rule.remediation.targetDocumentLambda.sourceFilePath} not found`, + ); + } + } + } + } + } + } + } + + if (errors.length) { + throw new Error(`${SecurityConfig.FILENAME} has ${errors.length} issues: ${errors.join(' ')}`); + } + + Object.assign(this, values); + } + } + + /** + * Return delegated-admin-account name + */ + public getDelegatedAccountName(): string { + return this.centralSecurityServices.delegatedAdminAccount; + } + + /** + * + * @param dir + * @returns + */ + static load(dir: string): SecurityConfig { + const buffer = fs.readFileSync(path.join(dir, SecurityConfig.FILENAME), 'utf8'); + const values = t.parse(SecurityConfigTypes.securityConfig, yaml.load(buffer)); + return new SecurityConfig(values, dir); + } + + /** + * Load from string content + * @param content + */ + static loadFromString(content: string): SecurityConfig | undefined { + try { + const values = t.parse(SecurityConfigTypes.securityConfig, yaml.load(content)); + return new SecurityConfig(values); + } catch (e) { + console.log('[security-config] Error parsing input, global config undefined'); + console.log(`${e}`); + return undefined; + } + } +} diff --git a/source/packages/@aws-accelerator/config/package.json b/source/packages/@aws-accelerator/config/package.json new file mode 100644 index 000000000..87fd9487c --- /dev/null +++ b/source/packages/@aws-accelerator/config/package.json @@ -0,0 +1,49 @@ +{ + "name": "@aws-accelerator/config", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "tsc", + "watch": "tsc -w", + "test": "jest --coverage --ci", + "lint": "eslint --fix --max-warnings 0 -c ../../../.eslintrc.json 'lib/**/*.{ts,tsx}' 'test/**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../.eslintrc.json 'lib/**/*.{ts,tsx}' 'test/**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "@typescript-eslint/eslint-plugin": "5.6.0", + "@typescript-eslint/parser": "5.6.0", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "ts-node": "10.4.0", + "typescript": "4.5.2" + }, + "peerDependencies": { + "eslint": "8.4.1" + }, + "dependencies": { + "fp-ts": "2.11.5", + "io-ts": "2.2.16", + "ip-num": "1.3.3" + } +} diff --git a/source/packages/@aws-accelerator/config/test/organization.test.ts b/source/packages/@aws-accelerator/config/test/organization.test.ts new file mode 100644 index 000000000..5274fea0a --- /dev/null +++ b/source/packages/@aws-accelerator/config/test/organization.test.ts @@ -0,0 +1,24 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// import { countResources, expect as expectCDK } from '@aws-cdk/assert'; + +test('Organization Config', () => { + // // const app = new cdk.App(); + // // // WHEN + // // const stack = new Installer.InstallerStack(app, 'MyTestStack'); + // // // THEN + // // // expectCDK(stack).to(matchTemplate({ + // // // "Resources": {} + // // // }, MatchStyle.EXACT)) +}); diff --git a/source/packages/@aws-accelerator/config/tsconfig.json b/source/packages/@aws-accelerator/config/tsconfig.json new file mode 100644 index 000000000..fc62a1e09 --- /dev/null +++ b/source/packages/@aws-accelerator/config/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["lib/**/*", "index.ts"], + "exclude": ["test/**/*"] +} diff --git a/source/packages/@aws-accelerator/constructs/.gitignore b/source/packages/@aws-accelerator/constructs/.gitignore new file mode 100644 index 000000000..69d4bb7af --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/.gitignore @@ -0,0 +1,9 @@ +*.js +*.js.map +!jest.config.js +*.d.ts +node_modules + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/source/packages/@aws-accelerator/constructs/.npmignore b/source/packages/@aws-accelerator/constructs/.npmignore new file mode 100644 index 000000000..c1d6d45dc --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/.npmignore @@ -0,0 +1,6 @@ +*.ts +!*.d.ts + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/source/packages/@aws-accelerator/constructs/README.md b/source/packages/@aws-accelerator/constructs/README.md new file mode 100644 index 000000000..7c6ea1745 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/README.md @@ -0,0 +1 @@ +# @aws-accelerator/constructs diff --git a/source/packages/@aws-accelerator/constructs/index.ts b/source/packages/@aws-accelerator/constructs/index.ts new file mode 100644 index 000000000..d0f64a28d --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/index.ts @@ -0,0 +1,70 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +export * from './lib/aws-budgets/budget-definition'; +export * from './lib/aws-controltower/create-accounts'; +export * from './lib/aws-cur/report-definition'; +export * from './lib/aws-ec2/delete-default-vpc'; +export * from './lib/aws-ec2/dhcp-options'; +export * from './lib/aws-ec2/ebs-encryption'; +export * from './lib/aws-ec2/prefix-list'; +export * from './lib/aws-ec2/route-table'; +export * from './lib/aws-ec2/transit-gateway'; +export * from './lib/aws-ec2/transit-gateway-route-table'; +export * from './lib/aws-ec2/transit-gateway-static-route'; +export * from './lib/aws-ec2/vpc'; +export * from './lib/aws-ec2/vpc-endpoint'; +export * from './lib/aws-ec2/vpc-peering'; +export * from './lib/aws-guardduty/guardduty-detector-config'; +export * from './lib/aws-guardduty/guardduty-members'; +export * from './lib/aws-guardduty/guardduty-organization-admin-account'; +export * from './lib/aws-guardduty/guardduty-publishing-destination'; +export * from './lib/aws-iam/password-policy'; +export * from './lib/aws-kms/key-lookup'; +export * from './lib/aws-macie/macie-export-config-classification'; +export * from './lib/aws-macie/macie-members'; +export * from './lib/aws-macie/macie-organization-admin-account'; +export * from './lib/aws-macie/macie-session'; +export * from './lib/aws-networkfirewall/firewall'; +export * from './lib/aws-networkfirewall/policy'; +export * from './lib/aws-networkfirewall/rule-group'; +export * from './lib/aws-organizations/account'; +export * from './lib/aws-organizations/create-accounts'; +export * from './lib/aws-organizations/enable-aws-service-access'; +export * from './lib/aws-organizations/enable-policy-type'; +export * from './lib/aws-organizations/organization'; +export * from './lib/aws-organizations/organizational-units'; +export * from './lib/aws-organizations/policy'; +export * from './lib/aws-organizations/policy-attachment'; +export * from './lib/aws-organizations/register-delegated-administrator'; +export * from './lib/aws-ram/enable-sharing-with-aws-organization'; +export * from './lib/aws-ram/resource-share'; +export * from './lib/aws-route-53-resolver/firewall-domain-list'; +export * from './lib/aws-route-53-resolver/firewall-rule-group'; +export * from './lib/aws-route-53-resolver/query-logging-config'; +export * from './lib/aws-route-53-resolver/resolver-endpoint'; +export * from './lib/aws-route-53-resolver/resolver-rule'; +export * from './lib/aws-route-53/associate-hosted-zones'; +export * from './lib/aws-route-53/hosted-zone'; +export * from './lib/aws-route-53/record-set'; +export * from './lib/aws-s3/bucket'; +export * from './lib/aws-s3/central-logs-bucket'; +export * from './lib/aws-s3/public-access-block'; +export * from './lib/aws-securityhub/securityhub-members'; +export * from './lib/aws-securityhub/securityhub-organization-admin-account'; +export * from './lib/aws-securityhub/securityhub-standards'; +export * from './lib/aws-servicecatalog/get-portfolio-id'; +export * from './lib/aws-ssm/document'; +export * from './lib/aws-ssm/session-manager-settings'; +export * from './lib/aws-ssm/ssm-parameter'; +export * from './lib/aws-ssm/ssm-parameter-lookup'; diff --git a/source/packages/@aws-accelerator/constructs/jest.config.js b/source/packages/@aws-accelerator/constructs/jest.config.js new file mode 100644 index 000000000..0e8a6842d --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/jest.config.js @@ -0,0 +1,6 @@ +const base = require('../../../jest.config.base'); +const packageJson = require('./package.json'); + +module.exports = { + ...base.getJestJunitConfig(packageJson.name), +}; diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-budgets/budget-definition.ts b/source/packages/@aws-accelerator/constructs/lib/aws-budgets/budget-definition.ts new file mode 100644 index 000000000..073fa9507 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-budgets/budget-definition.ts @@ -0,0 +1,100 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +export interface BudgetDefinitionProps { + /** + * List of Budgets. + */ + readonly budgets?: { + type: 'COST' | 'RI_UTILIZATION' | 'RI_COVERAGE' | 'SAVINGS_PLAN_UTILIZATION' | 'SAVINGS_PLAN_COVERAGE' | string; + timeUnit: 'DAILY' | 'WEEKLY' | 'MONTHLY' | string; + amount: number; + unit: 'USD' | string; + name: string; + includeCredit: boolean; + includeDiscount: boolean; + includeOtherSubscription: boolean; + includeRecurring: boolean; + includeRefund: boolean; + includeSubscription: boolean; + includeSupport: boolean; + includeTax: boolean; + includeUpfront: boolean; + useAmortized: boolean; + useBlended: boolean; + notifications: { + threshold: number; + thresholdType: 'PERCENTAGE' | 'ABSOLUTE_VALUE' | string; + type: 'ACTUAL' | 'FORECASTED' | string; + comparisonOperator: 'GREATER_THAN' | 'LESS_THAN' | string; + address: string; + subscriptionType: 'EMAIL' | 'SNS' | string; + }[]; + }[]; +} + +export class BudgetDefinition extends cdk.Resource { + constructor(scope: Construct, id: string, props: BudgetDefinitionProps) { + super(scope, id); + + for (const budgetParameters of props.budgets ?? []) { + const notificationsWithSubscribers = []; + const budget = { + budgetType: budgetParameters.type, + timeUnit: budgetParameters.timeUnit, + budgetLimit: { + amount: budgetParameters.amount, + unit: budgetParameters.unit, + }, + budgetName: budgetParameters.name, + costTypes: { + includeCredit: budgetParameters.includeCredit ?? undefined, + includeDiscount: budgetParameters.includeDiscount ?? undefined, + includeOtherSubscription: budgetParameters.includeOtherSubscription ?? undefined, + includeRecurring: budgetParameters.includeRecurring ?? undefined, + includeRefund: budgetParameters.includeRefund ?? undefined, + includeSubscription: budgetParameters.includeSubscription ?? undefined, + includeSupport: budgetParameters.includeSupport ?? undefined, + includeTax: budgetParameters.includeTax ?? undefined, + includeUpfront: budgetParameters.includeUpfront ?? undefined, + useAmortized: budgetParameters.useAmortized ?? undefined, + useBlended: budgetParameters.useBlended ?? undefined, + }, + }; + for (const notify of budgetParameters.notifications ?? []) { + const notificationWithSubscriber = { + notification: { + comparisonOperator: notify.comparisonOperator, + notificationType: notify.type, + threshold: notify.threshold, + thresholdType: notify.thresholdType ?? undefined, + }, + subscribers: [ + { + address: notify.address, + subscriptionType: notify.subscriptionType, + }, + ], + }; + notificationsWithSubscribers.push(notificationWithSubscriber); + } + new cdk.aws_budgets.CfnBudget(this, `${budget.budgetName}`, { + budget, + notificationsWithSubscribers, + }); + } + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-controltower/create-accounts-status/index.ts b/source/packages/@aws-accelerator/constructs/lib/aws-controltower/create-accounts-status/index.ts new file mode 100644 index 000000000..b39cd239f --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-controltower/create-accounts-status/index.ts @@ -0,0 +1,282 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +/** + * aws-controltower-create-accounts-status - lambda handler + * + * @param event + * @returns + */ + +import * as AWS from 'aws-sdk'; +import { throttlingBackOff } from '@aws-accelerator/utils'; +import { v4 as uuidv4 } from 'uuid'; + +const documentClient = new AWS.DynamoDB.DocumentClient(); +const serviceCatalogClient = new AWS.ServiceCatalog(); + +const tableName = process.env['NewAccountsTableName'] ?? ''; + +interface AccountConfig { + name: string; + description: string; + email: string; + enableGovCloud?: boolean | undefined; + organizationalUnitId: string | undefined; + createRequestId?: string | undefined; +} + +type AccountConfigs = Array; + +//eslint-disable-next-line @typescript-eslint/no-explicit-any +export async function handler(event: any): Promise< + | { + IsComplete: boolean; + } + | undefined +> { + console.log(event); + // if provisioning is in progress return + // we cannot provision another account while + // an account is being provisioned + if ((await inProgress()) === true) { + console.log('Account provisioning in progress continuing to wait'); + return { + IsComplete: false, + }; + } + + try { + //get a single accountConfig from table and attempt to provision + //if no record is returned then all new accounts are provisioned + const accountToAdd: AccountConfigs = await getSingleAccountConfigFromTable(); + if (accountToAdd.length === 0) { + //check if any accounts in error or tainted state + if (await provisionSuccess()) { + console.log('Control Tower account provisioning complete.'); + } else { + console.log('Control Tower account provisioning failed.'); + throw new Error('Accounts failed to enroll in Control Tower. Check Service Catalog Console'); + } + + return { + IsComplete: true, + }; + } + + const provisionResponse = await provisionAccount(accountToAdd[0]); + console.log(`Provision response: ${JSON.stringify(provisionResponse)}`); + + const deleteResponse = await deleteSingleAccountConfigFromTable(accountToAdd[0].email); + console.log(`Delete response: ${JSON.stringify(deleteResponse)}`); + + return { + IsComplete: false, + }; + } catch (e) { + console.log(e); + console.log(`Create accounts failed. Deleting pending account creation records`); + await deleteAllRecordsFromTable(tableName); + throw new Error(`Account creation failed. ${e}`); + } +} + +async function inProgress(): Promise { + const provisionedProductsUnderChange: AWS.ServiceCatalog.ProvisionedProductAttribute[] = + await getProvisionedProductsWithStatus('UNDER_CHANGE'); + if (provisionedProductsUnderChange.length > 0) { + console.log('Products are UNDER_CHANGE'); + return true; + } + + const provisionedProductsPlan: AWS.ServiceCatalog.ProvisionedProductAttribute[] = + await getProvisionedProductsWithStatus('PLAN_IN_PROGRESS'); + if (provisionedProductsPlan.length > 0) { + console.log('Products are PLAN_IN_PROGRESS'); + return true; + } + + return false; +} + +async function provisionSuccess(): Promise { + const provisionedProductsError: AWS.ServiceCatalog.ProvisionedProductAttribute[] = + await getProvisionedProductsWithStatus('ERROR'); + if (provisionedProductsError.length > 0) { + console.log(`Provisioning failure error message: ${provisionedProductsError[0].StatusMessage}`); + return false; + } + + const provisionedProductsTainted: AWS.ServiceCatalog.ProvisionedProductAttribute[] = + await getProvisionedProductsWithStatus('TAINTED'); + if (provisionedProductsTainted.length > 0) { + return false; + } + + return true; +} + +async function getProvisionedProductsWithStatus( + searchStatus: string, +): Promise { + const provisionedProducts: AWS.ServiceCatalog.ProvisionedProductAttribute[] = []; + let nextToken: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => + serviceCatalogClient + .searchProvisionedProducts({ + Filters: { + SearchQuery: [`status: ${searchStatus}`], + }, + AccessLevelFilter: { + Key: 'Account', + Value: 'self', + }, + PageToken: nextToken, + }) + .promise(), + ); + + for (const product of page.ProvisionedProducts ?? []) { + if (product.Type === 'CONTROL_TOWER_ACCOUNT') { + provisionedProducts.push(product); + } + } + nextToken = page.NextPageToken; + } while (nextToken); + + return provisionedProducts; +} + +async function getSingleAccountConfigFromTable(): Promise { + const accountToAdd: AccountConfigs = []; + const scanParams = { + TableName: tableName, + Limit: 1, + }; + const response = await throttlingBackOff(() => documentClient.scan(scanParams).promise()); + + console.log(`getSingleAccount response ${JSON.stringify(response)}`); + if (response.Items?.length ?? 0 > 0) { + const account: AccountConfig = JSON.parse(response.Items![0]['accountConfig']); + accountToAdd.push(account); + console.log(`Account to add ${JSON.stringify(accountToAdd)}`); + } + return accountToAdd; +} + +async function deleteSingleAccountConfigFromTable(accountToDeleteEmail: string): Promise { + const deleteParams = { + TableName: tableName, + Key: { + accountEmail: accountToDeleteEmail, + }, + }; + const response = await throttlingBackOff(() => documentClient.delete(deleteParams).promise()); + return JSON.stringify(response); +} + +async function provisionAccount(accountToAdd: AccountConfig): Promise { + const provisionToken = uuidv4(); + + const searchProductsCommandOutput = await throttlingBackOff(() => + serviceCatalogClient + .searchProducts({ Filters: { FullTextSearch: ['AWS Control Tower Account Factory'] } }) + .promise(), + ); + + const productId = searchProductsCommandOutput?.ProductViewSummaries?.[0]?.ProductId ?? ''; + + console.log(`Service Catalog ProductId ${productId}`); + + const listProvisioningArtifactsOutput = await throttlingBackOff(() => + serviceCatalogClient.listProvisioningArtifacts({ ProductId: productId }).promise(), + ); + + const provisioningArtifact = listProvisioningArtifactsOutput?.ProvisioningArtifactDetails?.find(a => a.Active); + const provisioningArtifactId = provisioningArtifact?.Id; + console.log(`Service Catalog Provisioning Artifact Id ${provisioningArtifactId}`); + + const provisionInput = { + ProductName: 'AWS Control Tower Account Factory', + ProvisionToken: provisionToken, + ProvisioningArtifactId: provisioningArtifactId, + ProvisionedProductName: accountToAdd.name, + ProvisioningParameters: [ + { + Key: 'AccountName', + Value: accountToAdd.name, + }, + { + Key: 'AccountEmail', + Value: accountToAdd.email, + }, + { + Key: 'ManagedOrganizationalUnit', + Value: accountToAdd.organizationalUnitId, + }, + { + Key: 'SSOUserEmail', + Value: accountToAdd.email, + }, + { + Key: 'SSOUserFirstName', + Value: accountToAdd.name, + }, + { + Key: 'SSOUserLastName', + Value: accountToAdd.name, + }, + { + Key: 'VPCOptions', + Value: 'No-Primary-VPC', + }, + { + Key: 'VPCRegion', + Value: 'us-east-1', //dummy value, vpc is not created + }, + { + Key: 'VPCCidr', + Value: '10.0.0.0/16', //dummy value, vpc is not created + }, + { + Key: 'PeerVPC', + Value: 'false', //dummy value, vpc is not created + }, + ], + }; + + const response: AWS.ServiceCatalog.ProvisionProductOutput = await throttlingBackOff(() => + serviceCatalogClient.provisionProduct(provisionInput).promise(), + ); + return response; +} + +async function deleteAllRecordsFromTable(tableName: string) { + const params = { + TableName: tableName, + ProjectionExpression: 'accountEmail', + }; + const response = await documentClient.scan(params).promise(); + if (response.Items) { + for (const item of response.Items) { + console.log(item['accountEmail']); + const params = { + TableName: tableName, + Key: { + accountEmail: item['accountEmail'], + }, + }; + await documentClient.delete(params).promise(); + } + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-controltower/create-accounts-status/package.json b/source/packages/@aws-accelerator/constructs/lib/aws-controltower/create-accounts-status/package.json new file mode 100644 index 000000000..d2a753233 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-controltower/create-accounts-status/package.json @@ -0,0 +1,46 @@ +{ + "name": "@aws-accelerator/constructs-aws-control-tower-create-accounts-status", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --sourcemap --platform=node --target=node14 --external:aws-sdk --outfile=./dist/index.js index.ts", + "test": "", + "testreport": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2", + "ts-node": "10.4.0" + }, + "peerDependencies": { + "eslint": "8.4.1" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1001.0" + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-controltower/create-accounts-status/tsconfig.json b/source/packages/@aws-accelerator/constructs/lib/aws-controltower/create-accounts-status/tsconfig.json new file mode 100644 index 000000000..ebcb98a79 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-controltower/create-accounts-status/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-controltower/create-accounts.ts b/source/packages/@aws-accelerator/constructs/lib/aws-controltower/create-accounts.ts new file mode 100644 index 000000000..7d67d51e7 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-controltower/create-accounts.ts @@ -0,0 +1,177 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { v4 as uuidv4 } from 'uuid'; +import { Construct } from 'constructs'; + +import path = require('path'); + +/** + * Control create accounts + */ +export interface CreateControlTowerAccountsProps { + readonly table: cdk.aws_dynamodb.ITable; + readonly portfolioId: string; + /** + * Custom resource lambda log group encryption key + */ + readonly kmsKey: cdk.aws_kms.Key; + /** + * Custom resource lambda log retention in days + */ + readonly logRetentionInDays: number; +} + +/** + * Class Create Accounts when Control Tower Enabled + */ +export class CreateControlTowerAccounts extends Construct { + readonly onEvent: cdk.aws_lambda.IFunction; + readonly isComplete: cdk.aws_lambda.IFunction; + readonly provider: cdk.custom_resources.Provider; + readonly id: string; + + constructor(scope: Construct, id: string, props: CreateControlTowerAccountsProps) { + super(scope, id); + + const CREATE_CONTROL_TOWER_ACCOUNTS = 'Custom::CreateControlTowerAccounts'; + + this.onEvent = new cdk.aws_lambda.Function(this, 'CreateControlTowerAccount', { + code: cdk.aws_lambda.Code.fromAsset(path.join(__dirname, 'create-accounts/dist')), + runtime: cdk.aws_lambda.Runtime.NODEJS_14_X, + handler: 'index.handler', + timeout: cdk.Duration.minutes(1), + description: 'Create Control Tower Account onEvent handler', + environmentEncryption: props.kmsKey, + }); + new cdk.aws_logs.LogGroup(this, `${this.onEvent.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${this.onEvent.functionName}`, + retention: props.logRetentionInDays, + encryptionKey: props.kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + const ddbPolicy = new cdk.aws_iam.PolicyStatement({ + sid: 'DynamoDb', + effect: cdk.aws_iam.Effect.ALLOW, + actions: ['dynamodb:Scan', 'dynamodb:GetItem', 'dynamodb:DeleteItem'], + resources: [props.table.tableArn], + }); + const ddbKmsPolicy = new cdk.aws_iam.PolicyStatement({ + sid: 'KMS', + effect: cdk.aws_iam.Effect.ALLOW, + actions: ['kms:Encrypt', 'kms:Decrypt', 'kms:GenerateDataKey*', 'kms:DescribeKey'], + resources: [props.table.encryptionKey?.keyArn as string], + }); + const scPolicy = new cdk.aws_iam.PolicyStatement({ + sid: 'ServiceCatalog', + effect: cdk.aws_iam.Effect.ALLOW, + actions: [ + 'servicecatalog:SearchProvisionedProducts', + 'servicecatalog:ProvisionProduct', + 'servicecatalog:DescribeProduct', + 'servicecatalog:ListProvisioningArtifacts', + 'servicecatalog:DescribeProvisionedProduct', + ], + resources: ['*'], + }); + const ctPolicy = new cdk.aws_iam.PolicyStatement({ + sid: 'ControlTower', + effect: cdk.aws_iam.Effect.ALLOW, + actions: [ + 'controltower:CreateManagedAccount', + 'controltower:SetupLandingZone', + 'controltower:EnableGuardrail', + 'controltower:Describe*', + 'controltower:Get*', + 'controltower:List*', + ], + resources: ['*'], + }); + const ssoPolicy = new cdk.aws_iam.PolicyStatement({ + sid: 'SSO', + effect: cdk.aws_iam.Effect.ALLOW, + actions: [ + 'sso-directory:DescribeDirectory', + 'sso-directory:CreateUser', + 'sso-directory:SearchUsers', + 'sso-directory:SearchGroups', + 'sso:ListDirectoryAssociations', + 'sso:DescribeRegisteredRegions', + 'sso:ListProfileAssociations', + 'sso:AssociateProfile', + 'sso:GetProfile', + 'sso:CreateProfile', + 'sso:UpdateProfile', + 'sso:GetTrust', + 'sso:CreateTrust', + 'sso:UpdateTrust', + 'sso:GetApplicationInstance', + 'sso:CreateApplicationInstance', + 'sso:ListPermissionSets', + 'sso:GetSSOStatus', + ], + resources: ['*'], + }); + this.isComplete = new cdk.aws_lambda.Function(this, 'CreateControlTowerAccountStatus', { + code: cdk.aws_lambda.Code.fromAsset(path.join(__dirname, 'create-accounts-status/dist')), + runtime: cdk.aws_lambda.Runtime.NODEJS_14_X, + handler: 'index.handler', + timeout: cdk.Duration.minutes(5), + description: 'Create Control Tower Account isComplete handler', + environment: { NewAccountsTableName: props.table.tableName }, + initialPolicy: [ddbPolicy, ddbKmsPolicy, ctPolicy, ssoPolicy], + environmentEncryption: props.kmsKey, + }); + new cdk.aws_logs.LogGroup(this, `${this.isComplete.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${this.isComplete.functionName}`, + retention: props.logRetentionInDays, + encryptionKey: props.kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + this.isComplete.role?.addManagedPolicy( + cdk.aws_iam.ManagedPolicy.fromAwsManagedPolicyName('AWSServiceCatalogEndUserFullAccess'), + ); + this.isComplete.addToRolePolicy(scPolicy); + + new cdk.aws_servicecatalog.CfnPortfolioPrincipalAssociation(this, 'LambdaPrincipalAssociation', { + portfolioId: props.portfolioId, + principalArn: this.isComplete.role?.roleArn ?? '', + principalType: 'IAM', + }); + + this.provider = new cdk.custom_resources.Provider(this, 'CreateControlTowerAcccountsProvider', { + onEventHandler: this.onEvent, + isCompleteHandler: this.isComplete, + queryInterval: cdk.Duration.minutes(1), + totalTimeout: cdk.Duration.hours(4), + }); + + // + // Custom Resource definition. We want this resource to be evaluated on + // every CloudFormation update, so we generate a new uuid to force + // re-evaluation. + // + const resource = new cdk.CustomResource(this, 'Resource', { + resourceType: CREATE_CONTROL_TOWER_ACCOUNTS, + serviceToken: this.provider.serviceToken, + properties: { + uuid: uuidv4(), + }, + }); + + this.id = resource.ref; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-controltower/create-accounts/index.ts b/source/packages/@aws-accelerator/constructs/lib/aws-controltower/create-accounts/index.ts new file mode 100644 index 000000000..22e7f3f25 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-controltower/create-accounts/index.ts @@ -0,0 +1,39 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +/** + * aws-controltower-create-accounts - lambda handler + * + * @param event + * @returns + */ +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + IsComplete: boolean; + } + | undefined +> { + switch (event.RequestType) { + case 'Create': + case 'Update': + return { + IsComplete: false, + }; + + case 'Delete': + // Do Nothing + return { + IsComplete: true, + }; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-controltower/create-accounts/package.json b/source/packages/@aws-accelerator/constructs/lib/aws-controltower/create-accounts/package.json new file mode 100644 index 000000000..8d5af6d09 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-controltower/create-accounts/package.json @@ -0,0 +1,44 @@ +{ + "name": "@aws-accelerator/constructs-aws-control-tower-create-accounts", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --sourcemap --platform=node --target=node14 --external:aws-sdk --outfile=./dist/index.js index.ts", + "test": "", + "testreport": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2" + }, + "peerDependencies": { + "eslint": "8.4.1" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0" + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-controltower/create-accounts/tsconfig.json b/source/packages/@aws-accelerator/constructs/lib/aws-controltower/create-accounts/tsconfig.json new file mode 100644 index 000000000..ebcb98a79 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-controltower/create-accounts/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-cur/cross-region-report-definition/index.ts b/source/packages/@aws-accelerator/constructs/lib/aws-cur/cross-region-report-definition/index.ts new file mode 100644 index 000000000..92c5c09fa --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-cur/cross-region-report-definition/index.ts @@ -0,0 +1,102 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as AWS from 'aws-sdk'; + +import { throttlingBackOff } from '@aws-accelerator/utils'; + +AWS.config.logger = console; + +/** + * cross-region-report-definition - lambda handler + * + * @param event + * @returns + */ + +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + PhysicalResourceId: string; + Status: string; + } + | undefined +> { + interface ReportDefinition { + ReportName: string; + TimeUnit: string; + Format: string; + Compression: string; + S3Bucket: string; + S3Prefix: string; + S3Region: string; + AdditionalSchemaElements: string[]; + AdditionalArtifacts?: string[]; + RefreshClosedReports?: boolean; + ReportVersioning?: string; + BillingViewArn?: string; + } + + const reportDefinition: ReportDefinition = event.ResourceProperties['reportDefinition']; + const curClient = new AWS.CUR({ region: 'us-east-1' }); + + // Handle case where boolean is passed as string + if (reportDefinition.RefreshClosedReports) { + reportDefinition.RefreshClosedReports = returnBoolean(reportDefinition.RefreshClosedReports.toString()); + } + + switch (event.RequestType) { + case 'Create': + // Create new report definition + console.log(`Creating new report definition ${reportDefinition.ReportName}`); + await throttlingBackOff(() => curClient.putReportDefinition({ ReportDefinition: reportDefinition }).promise()); + + return { + PhysicalResourceId: reportDefinition.ReportName, + Status: 'SUCCESS', + }; + + case 'Update': + // Modify report definition + console.log(`Modifying report definition ${reportDefinition.ReportName}`); + await throttlingBackOff(() => + curClient + .modifyReportDefinition({ ReportName: reportDefinition.ReportName, ReportDefinition: reportDefinition }) + .promise(), + ); + + return { + PhysicalResourceId: event.PhysicalResourceId, + Status: 'SUCCESS', + }; + + case 'Delete': + // Delete report definition + console.log(`Deleting report definition ${event.PhysicalResourceId}`); + await throttlingBackOff(() => + curClient.deleteReportDefinition({ ReportName: event.PhysicalResourceId }).promise(), + ); + + return { + PhysicalResourceId: event.PhysicalResourceId, + Status: 'SUCCESS', + }; + } +} + +function returnBoolean(input: string): boolean | undefined { + try { + return JSON.parse(input.toLowerCase()); + } catch (e) { + return undefined; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-cur/cross-region-report-definition/package.json b/source/packages/@aws-accelerator/constructs/lib/aws-cur/cross-region-report-definition/package.json new file mode 100644 index 000000000..3feabc5fe --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-cur/cross-region-report-definition/package.json @@ -0,0 +1,44 @@ +{ + "name": "@aws-accelerator/constructs-aws-cur-cross-region-report-definition", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "description": "A custom resource to create a CUR report definition", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "private": true, + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --outfile=./dist/index.js --platform=node --target=node14 --external:aws-sdk index.ts", + "test": "", + "testreport": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "esbuild": "0.14.2", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1001.0" + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-cur/cross-region-report-definition/tsconfig.json b/source/packages/@aws-accelerator/constructs/lib/aws-cur/cross-region-report-definition/tsconfig.json new file mode 100644 index 000000000..ebcb98a79 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-cur/cross-region-report-definition/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-cur/report-definition.ts b/source/packages/@aws-accelerator/constructs/lib/aws-cur/report-definition.ts new file mode 100644 index 000000000..7f1132d53 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-cur/report-definition.ts @@ -0,0 +1,240 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import * as path from 'path'; + +export interface IReportDefinition extends cdk.IResource { + /** + * The name of the report that you want to create. + * @attribute + */ + readonly reportName: string; +} + +type Compression = 'ZIP' | 'GZIP' | 'Parquet' | string; + +type Format = 'textORcsv' | 'Parquet' | string; + +type ReportVersioning = 'CREATE_NEW_REPORT' | 'OVERWRITE_REPORT' | string; + +type TimeUnit = 'HOURLY' | 'DAILY' | 'MONTHLY' | string; + +type AdditionalArtifacts = 'REDSHIFT' | 'QUICKSIGHT' | 'ATHENA' | string; + +export interface ReportDefinitionProps { + /** + * The compression format that Amazon Web Services uses for the report. + * + */ + readonly compression: Compression; + + /** + * The format that Amazon Web Services saves the report in. + * + */ + readonly format: Format; + + /** + * Whether you want Amazon Web Services to update your reports after they have been finalized if + * Amazon Web Services detects charges related to previous months. + * + */ + readonly refreshClosedReports: boolean | cdk.IResolvable; + + /** + * The name of the report that you want to create. + * + * @default - A CDK generated name + */ + readonly reportName: string; + + /** + * Whether you want Amazon Web Services to overwrite the previous version of each report or to + * deliver the report in addition to the previous versions. + * + */ + readonly reportVersioning: ReportVersioning; + + /** + * The S3 bucket where Amazon Web Services delivers the report. + * + */ + readonly s3Bucket: cdk.aws_s3.IBucket; + + /** + * The prefix that Amazon Web Services adds to the report name when Amazon Web Services delivers the report. + * + */ + readonly s3Prefix: string; + + /** + * The Region of the S3 bucket that Amazon Web Services delivers the report into. + * + */ + readonly s3Region: string; + + /** + * The granularity of the line items in the report. + * + */ + readonly timeUnit: TimeUnit; + + /** + * A list of manifests that you want Amazon Web Services to create for this report. + * + * @default - no additional artifacts + */ + readonly additionalArtifacts?: AdditionalArtifacts[]; + + /** + * A list of strings that indicate additional content that Amazon Web Services includes in + * the report, such as individual resource IDs. + * + * @default - no additional schema elements + */ + readonly additionalSchemaElements?: string[]; + + /** + * The Amazon Resource Name (ARN) of the billing view. + * + * @default - no billing view ARN + */ + readonly billingViewArn?: string; + + /** + * Custom resource lambda log group encryption key + */ + readonly kmsKey: cdk.aws_kms.Key; + /** + * Custom resource lambda log retention in days + */ + readonly logRetentionInDays: number; +} + +export class ReportDefinition extends cdk.Resource implements IReportDefinition { + public readonly reportName: string; + + constructor(scope: Construct, id: string, props: ReportDefinitionProps) { + super(scope, id, { + physicalName: props.reportName, + }); + + this.reportName = this.physicalName; + + if (cdk.Stack.of(this).region === 'us-east-1') { + // Use native Cfn construct + new cdk.aws_cur.CfnReportDefinition(this, 'Resource', { + compression: props.compression, + format: props.format, + refreshClosedReports: props.refreshClosedReports, + reportName: props.reportName, + reportVersioning: props.reportVersioning, + s3Bucket: props.s3Bucket.bucketName, + s3Prefix: props.s3Prefix, + s3Region: props.s3Region, + timeUnit: props.timeUnit, + additionalArtifacts: props.additionalArtifacts, + additionalSchemaElements: props.additionalSchemaElements, + billingViewArn: props.billingViewArn, + }); + } else { + // Use custom resource + const provider = cdk.CustomResourceProvider.getOrCreateProvider(this, 'Custom::CrossRegionReportDefinition', { + codeDirectory: path.join(__dirname, 'cross-region-report-definition/dist'), + runtime: cdk.CustomResourceProviderRuntime.NODEJS_14_X, + policyStatements: [ + { + Effect: 'Allow', + Action: ['cur:DeleteReportDefinition', 'cur:ModifyReportDefinition', 'cur:PutReportDefinition'], + Resource: '*', + }, + ], + }); + + const resource = new cdk.CustomResource(this, 'Resource', { + resourceType: 'Custom::CrossRegionReportDefinition', + serviceToken: provider.serviceToken, + properties: { + reportDefinition: { + ReportName: props.reportName, + TimeUnit: props.timeUnit, + Format: props.format, + Compression: props.compression, + S3Bucket: props.s3Bucket.bucketName, + S3Prefix: props.s3Prefix, + S3Region: props.s3Region, + AdditionalSchemaElements: props.additionalSchemaElements ?? [], + AdditionalArtifacts: props.additionalArtifacts, + RefreshClosedReports: props.refreshClosedReports, + ReportVersioning: props.reportVersioning, + BillingViewArn: props.billingViewArn, + }, + }, + }); + + /** + * Singleton pattern to define the log group for the singleton function + * in the stack + */ + const stack = cdk.Stack.of(scope); + const logGroup = + (stack.node.tryFindChild(`${provider.node.id}LogGroup`) as cdk.aws_logs.LogGroup) ?? + new cdk.aws_logs.LogGroup(stack, `${provider.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${(provider.node.findChild('Handler') as cdk.aws_lambda.CfnFunction).ref}`, + retention: props.logRetentionInDays, + encryptionKey: props.kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + resource.node.addDependency(logGroup); + } + + // Add bucket policy + const policy = this.addBucketPolicy(props.s3Bucket); + this.node.addDependency(policy); + } + + private addBucketPolicy(bucket: cdk.aws_s3.IBucket): cdk.aws_s3.BucketPolicy { + const _stmt1: cdk.aws_iam.PolicyStatement = new cdk.aws_iam.PolicyStatement({ + effect: cdk.aws_iam.Effect.ALLOW, + actions: ['s3:GetBucketAcl', 's3:GetBucketPolicy'], + principals: [new cdk.aws_iam.ServicePrincipal('billingreports.amazonaws.com')], + resources: [bucket.bucketArn], + conditions: { + StringEquals: { + 'aws:SourceArn': `arn:aws:cur:us-east-1:${cdk.Aws.ACCOUNT_ID}:definition/*`, + 'aws:SourceAccount': `${cdk.Aws.ACCOUNT_ID}`, + }, + }, + }); + + const _stmt2: cdk.aws_iam.PolicyStatement = new cdk.aws_iam.PolicyStatement({ + effect: cdk.aws_iam.Effect.ALLOW, + actions: ['s3:PutObject'], + principals: [new cdk.aws_iam.ServicePrincipal('billingreports.amazonaws.com')], + resources: [bucket.arnForObjects('*')], + conditions: { + StringEquals: { + 'aws:SourceArn': `arn:aws:cur:us-east-1:${cdk.Aws.ACCOUNT_ID}:definition/*`, + 'aws:SourceAccount': `${cdk.Aws.ACCOUNT_ID}`, + }, + }, + }); + + bucket.addToResourcePolicy(_stmt1); + bucket.addToResourcePolicy(_stmt2); + + return bucket.policy!; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ec2/delete-default-vpc.ts b/source/packages/@aws-accelerator/constructs/lib/aws-ec2/delete-default-vpc.ts new file mode 100644 index 000000000..c3974d8f8 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ec2/delete-default-vpc.ts @@ -0,0 +1,94 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +const path = require('path'); + +/** + * Initialized DeleteDefaultVpcProps properties + */ +export interface DeleteDefaultVpcProps { + /** + * Custom resource lambda log group encryption key + */ + readonly kmsKey: cdk.aws_kms.Key; + /** + * Custom resource lambda log retention in days + */ + readonly logRetentionInDays: number; +} + +/** + * Class Delete the Default VPC + */ +export class DeleteDefaultVpc extends Construct { + readonly id: string; + constructor(scope: Construct, id: string, props: DeleteDefaultVpcProps) { + super(scope, id); + + const DELETE_DEFAULT_VPC_TYPE = 'Custom::DeleteDefaultVpc'; + + // + // Function definition for the custom resource + // + const provider = cdk.CustomResourceProvider.getOrCreateProvider(this, DELETE_DEFAULT_VPC_TYPE, { + codeDirectory: path.join(__dirname, 'delete-default-vpc/dist'), + runtime: cdk.CustomResourceProviderRuntime.NODEJS_14_X, + policyStatements: [ + { + Effect: 'Allow', + Action: [ + 'ec2:DeleteInternetGateway', + 'ec2:DetachInternetGateway', + 'ec2:DeleteNetworkAcl', + 'ec2:DeleteRoute', + 'ec2:DeleteSecurityGroup', + 'ec2:DeleteSubnet', + 'ec2:DeleteVpc', + 'ec2:DescribeInternetGateways', + 'ec2:DescribeNetworkAcls', + 'ec2:DescribeRouteTables', + 'ec2:DescribeSecurityGroups', + 'ec2:DescribeSubnets', + 'ec2:DescribeVpcs', + ], + Resource: '*', + }, + ], + }); + + const resource = new cdk.CustomResource(this, 'Resource', { + resourceType: DELETE_DEFAULT_VPC_TYPE, + serviceToken: provider.serviceToken, + }); + + /** + * Singleton pattern to define the log group for the singleton function + * in the stack + */ + const stack = cdk.Stack.of(scope); + const logGroup = + (stack.node.tryFindChild(`${provider.node.id}LogGroup`) as cdk.aws_logs.LogGroup) ?? + new cdk.aws_logs.LogGroup(stack, `${provider.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${(provider.node.findChild('Handler') as cdk.aws_lambda.CfnFunction).ref}`, + retention: props.logRetentionInDays, + encryptionKey: props.kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + resource.node.addDependency(logGroup); + + this.id = resource.ref; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ec2/delete-default-vpc/index.ts b/source/packages/@aws-accelerator/constructs/lib/aws-ec2/delete-default-vpc/index.ts new file mode 100644 index 000000000..afc3a652f --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ec2/delete-default-vpc/index.ts @@ -0,0 +1,239 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { throttlingBackOff } from '@aws-accelerator/utils'; +import * as AWS from 'aws-sdk'; +AWS.config.logger = console; + +/** + * delete-default-vpc - lambda handler + * + * @param event + * @returns + */ + +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + PhysicalResourceId: string | undefined; + Status: string; + } + | undefined +> { + // Retrieve operating region that stack is ran + const region = event.ResourceProperties['region']; + const ec2Client = new AWS.EC2({ region: region }); + const defaultVpcIds: string[] = []; + + switch (event.RequestType) { + case 'Create': + case 'Update': + console.log(`Starting - Deletion of default VPC and associated resources in ${region}`); + + // Retrieve default VPC(s) + let nextToken: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => + ec2Client + .describeVpcs({ + Filters: [{ Name: 'is-default', Values: [`true`] }], + NextToken: nextToken, + }) + .promise(), + ); + + for (const vpc of page.Vpcs ?? []) { + if (vpc.VpcId) { + defaultVpcIds.push(vpc.VpcId); + } + } + nextToken = page.NextToken; + } while (nextToken); + + console.log(`List of VPCs: `, defaultVpcIds); + if (defaultVpcIds.length == 0) { + console.warn('No default VPCs detected'); + return { + PhysicalResourceId: `delete-default-vpc`, + Status: 'SUCCESS', + }; + } else { + console.warn('Default VPC Detected'); + } + + // Retrieve and detach, delete IGWs + for (const vpcId of defaultVpcIds) { + let nextToken: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => + ec2Client + .describeInternetGateways({ + Filters: [{ Name: 'attachment.vpc-id', Values: [vpcId] }], + NextToken: nextToken, + }) + .promise(), + ); + + for (const igw of page.InternetGateways ?? []) { + for (const attachment of igw.Attachments ?? []) { + if (attachment.State == 'available') { + console.log(`Detaching ${igw.InternetGatewayId}`); + await throttlingBackOff(() => + ec2Client + .detachInternetGateway({ InternetGatewayId: igw.InternetGatewayId!, VpcId: vpcId }) + .promise(), + ); + } + console.warn(`${igw.InternetGatewayId} is not attached. Proceeding to delete.`); + await throttlingBackOff(() => + ec2Client + .deleteInternetGateway({ + InternetGatewayId: igw.InternetGatewayId!, + }) + .promise(), + ); + } + } + nextToken = page.NextToken; + } while (nextToken); + + // Retrieve Default VPC Subnets + console.log(`Gathering Subnets for VPC ${vpcId}`); + nextToken = undefined; + do { + const page = await throttlingBackOff(() => + ec2Client + .describeSubnets({ + Filters: [{ Name: 'vpc-id', Values: [vpcId] }], + NextToken: nextToken, + }) + .promise(), + ); + for (const subnet of page.Subnets ?? []) { + console.log(`Delete Subnet ${subnet.SubnetId}`); + await throttlingBackOff(() => + ec2Client + .deleteSubnet({ + SubnetId: subnet.SubnetId!, + }) + .promise(), + ); + } + nextToken = page.NextToken; + } while (nextToken); + + // Delete Routes + console.log(`Gathering list of Route Tables for VPC ${vpcId}`); + nextToken = undefined; + do { + const page = await throttlingBackOff(() => + ec2Client + .describeRouteTables({ + Filters: [{ Name: 'vpc-id', Values: [vpcId] }], + NextToken: nextToken, + }) + .promise(), + ); + for (const routeTableObject of page.RouteTables ?? []) { + for (const routes of routeTableObject.Routes ?? []) { + if (routes.GatewayId !== 'local') { + console.log(`Removing route ${routes.DestinationCidrBlock} from ${routeTableObject.RouteTableId}`); + await throttlingBackOff(() => + ec2Client + .deleteRoute({ + RouteTableId: routeTableObject.RouteTableId!, + DestinationCidrBlock: routes.DestinationCidrBlock, + }) + .promise(), + ); + } + } + } + nextToken = page.NextToken; + } while (nextToken); + + // List and Delete NACLs + console.log(`Gathering list of NACLs for VPC ${vpcId}`); + nextToken = undefined; + do { + const page = await throttlingBackOff(() => + ec2Client + .describeNetworkAcls({ + Filters: [{ Name: 'vpc-id', Values: [vpcId] }], + NextToken: nextToken, + }) + .promise(), + ); + for (const networkAclObject of page.NetworkAcls ?? []) { + if (networkAclObject.IsDefault !== true) { + console.log(`Deleting Network ACL ID ${networkAclObject.NetworkAclId}`); + await throttlingBackOff(() => + ec2Client + .deleteNetworkAcl({ + NetworkAclId: networkAclObject.NetworkAclId!, + }) + .promise(), + ); + } else { + console.warn(`${networkAclObject.NetworkAclId} is the default NACL. Ignoring`); + } + } + nextToken = page.NextToken; + } while (nextToken); + + // List and Delete Security Groups + console.log(`Gathering list of Security Groups for VPC ${vpcId}`); + nextToken = undefined; + do { + const page = await throttlingBackOff(() => + ec2Client + .describeSecurityGroups({ + Filters: [{ Name: 'vpc-id', Values: [vpcId] }], + NextToken: nextToken, + }) + .promise(), + ); + for (const securityGroupObject of page.SecurityGroups ?? []) { + if (securityGroupObject.GroupName == 'default') { + console.warn(`${securityGroupObject.GroupId} is the default SG. Ignoring`); + } else { + console.log(`Deleting Security Group Id ${securityGroupObject.GroupId}`); + await throttlingBackOff(() => + ec2Client + .deleteSecurityGroup({ + GroupId: securityGroupObject.GroupId, + }) + .promise(), + ); + } + } + nextToken = page.NextToken; + } while (nextToken); + + // Once all resources are deleted, delete the VPC. + console.log(`Deleting VPC ${vpcId}`); + await throttlingBackOff(() => ec2Client.deleteVpc({ VpcId: vpcId }).promise()); + } + + return { + PhysicalResourceId: `delete-default-vpc`, + Status: 'SUCCESS', + }; + + case 'Delete': + // Do Nothing + return { + PhysicalResourceId: event.PhysicalResourceId, + Status: 'SUCCESS', + }; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ec2/delete-default-vpc/package.json b/source/packages/@aws-accelerator/constructs/lib/aws-ec2/delete-default-vpc/package.json new file mode 100644 index 000000000..0344e531e --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ec2/delete-default-vpc/package.json @@ -0,0 +1,45 @@ +{ + "name": "@aws-accelerator/constructs-aws-ec2-delete-default-vpc", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --outfile=./dist/index.js --platform=node --target=node14 --external:aws-sdk index.ts", + "test": "", + "testreport": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2" + }, + "peerDependencies": { + "eslint": "8.4.1" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1001.0" + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ec2/delete-default-vpc/tsconfig.json b/source/packages/@aws-accelerator/constructs/lib/aws-ec2/delete-default-vpc/tsconfig.json new file mode 100644 index 000000000..ebcb98a79 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ec2/delete-default-vpc/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ec2/dhcp-options.ts b/source/packages/@aws-accelerator/constructs/lib/aws-ec2/dhcp-options.ts new file mode 100644 index 000000000..253007144 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ec2/dhcp-options.ts @@ -0,0 +1,92 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { CfnTag, IResource, Resource } from 'aws-cdk-lib'; +import * as ec2 from 'aws-cdk-lib/aws-ec2'; +import { Construct } from 'constructs'; + +export interface IDhcpOptions extends IResource { + /** + * The name of the DHCP options set. + */ + readonly name: string; + + /** + * The ID of the DHCP options set. + */ + readonly dhcpOptionsId: string; +} + +export interface DhcpOptionsProps { + /** + * The name of the DHCP options set. + */ + readonly name: string; + + /** + * This value is used to complete unqualified DNS hostnames. + */ + readonly domainName?: string; + + /** + * The IPv4 addresses of up to four domain name servers. + * + * @default -- AmazonProvidedDNS + */ + readonly domainNameServers?: string[]; + + /** + * The IPv4 addresses of up to four NetBIOS name servers. + */ + readonly netbiosNameServers?: string[]; + + /** + * The NetBIOS node type (1, 2, 4, or 8). + */ + readonly netbiosNodeType?: number; + + /** + * The IPv4 addresses of up to four Network Time Protocol (NTP) servers. + */ + readonly ntpServers?: string[]; + + /** + * Any tags assigned to the DHCP options set. + */ + readonly tags?: CfnTag[]; +} + +export class DhcpOptions extends Resource implements IDhcpOptions { + public readonly name: string; + public readonly dhcpOptionsId: string; + + constructor(scope: Construct, id: string, props: DhcpOptionsProps) { + super(scope, id); + + this.name = props.name; + + // Add name tag to tags + props.tags?.push({ key: 'Name', value: this.name }); + + const resource = new ec2.CfnDHCPOptions(this, 'Resource', { + domainName: props.domainName, + domainNameServers: props.domainNameServers ?? ['AmazonProvidedDNS'], + netbiosNameServers: props.netbiosNameServers, + netbiosNodeType: props.netbiosNodeType, + ntpServers: props.ntpServers, + tags: props.tags, + }); + + this.dhcpOptionsId = resource.attrDhcpOptionsId; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ec2/ebs-default-encryption/index.ts b/source/packages/@aws-accelerator/constructs/lib/aws-ec2/ebs-default-encryption/index.ts new file mode 100644 index 000000000..627a1dea0 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ec2/ebs-default-encryption/index.ts @@ -0,0 +1,54 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +/** + * aws-ec2-eanble-ebs-encryption - lambda handler + * + * @param event + * @returns + */ + +import * as AWS from 'aws-sdk'; +import { throttlingBackOff } from '@aws-accelerator/utils'; + +const ec2 = new AWS.EC2(); +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + Status: string | undefined; + StatusCode: number | undefined; + } + | undefined +> { + const kmsKeyId = event.ResourceProperties['kmsKeyId'] || undefined; + + switch (event.RequestType) { + case 'Create': + case 'Update': + const enableResponse = await throttlingBackOff(() => ec2.enableEbsEncryptionByDefault({}).promise()); + console.log(`Enable encryption response ${enableResponse}`); + + if (kmsKeyId) { + const response = await throttlingBackOff(() => ec2.modifyEbsDefaultKmsKeyId({ KmsKeyId: kmsKeyId }).promise()); + console.log(`Modify KMS Key response ${response}`); + } else { + const response = await throttlingBackOff(() => ec2.resetEbsDefaultKmsKeyId({}).promise()); + console.log(`Modify KMS Key response ${response}`); + } + return { Status: 'Success', StatusCode: 200 }; + + case 'Delete': + const disableResponse = await throttlingBackOff(() => ec2.disableEbsEncryptionByDefault({}).promise()); + console.log(`Modify KMS Key response ${disableResponse}`); + return { Status: 'Success', StatusCode: 200 }; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ec2/ebs-default-encryption/package.json b/source/packages/@aws-accelerator/constructs/lib/aws-ec2/ebs-default-encryption/package.json new file mode 100644 index 000000000..81327f5eb --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ec2/ebs-default-encryption/package.json @@ -0,0 +1,45 @@ +{ + "name": "@aws-accelerator/constructs-aws-ec2-enable-ebs-encryption", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --outfile=./dist/index.js --platform=node --target=node14 --external:aws-sdk index.ts", + "test": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "esbuild": "0.14.2", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2" + }, + "peerDependencies": { + "eslint": "8.4.1" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1001.0" + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ec2/ebs-default-encryption/tsconfig.json b/source/packages/@aws-accelerator/constructs/lib/aws-ec2/ebs-default-encryption/tsconfig.json new file mode 100644 index 000000000..272282f1f --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ec2/ebs-default-encryption/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} \ No newline at end of file diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ec2/ebs-encryption.ts b/source/packages/@aws-accelerator/constructs/lib/aws-ec2/ebs-encryption.ts new file mode 100644 index 000000000..dfe0fde1e --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ec2/ebs-encryption.ts @@ -0,0 +1,99 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +const path = require('path'); + +/** + * Initialized EbsVolumeEncryptionProps properties + */ +export interface EbsVolumeEncryptionProps { + /** + * Ebs encryption key + */ + readonly ebsEncryptionKmsKey: cdk.aws_kms.IKey; + /** + * Custom resource lambda log group encryption key + */ + readonly logGroupKmsKey: cdk.aws_kms.Key; + /** + * Custom resource lambda log retention in days + */ + readonly logRetentionInDays: number; +} + +/** + * Class to Enable Default EBS Volume Encryption + */ +export class EbsDefaultEncryption extends Construct { + public readonly id: string; + + constructor(scope: Construct, id: string, props: EbsVolumeEncryptionProps) { + super(scope, id); + + const EBS_ENCRYPTION_TYPE = 'Custom::EbsDefaultVolumeEncryption'; + + const iamPolicy = [ + { + Sid: 'EC2', + Effect: 'Allow', + Action: [ + 'ec2:DisableEbsEncryptionByDefault', + 'ec2:EnableEbsEncryptionByDefault', + 'ec2:ModifyEbsDefaultKmsKeyId', + 'ec2:ResetEbsDefaultKmsKeyId', + ], + Resource: '*', + }, + { + Sid: 'KMS', + Effect: 'Allow', + Action: ['kms:DescribeKey'], + Resource: props.ebsEncryptionKmsKey.keyArn, + }, + ]; + + const provider = cdk.CustomResourceProvider.getOrCreateProvider(this, EBS_ENCRYPTION_TYPE, { + codeDirectory: path.join(__dirname, 'ebs-default-encryption/dist'), + runtime: cdk.CustomResourceProviderRuntime.NODEJS_14_X, + policyStatements: iamPolicy, + }); + + const resource = new cdk.CustomResource(this, 'Resource', { + resourceType: EBS_ENCRYPTION_TYPE, + serviceToken: provider.serviceToken, + properties: { + kmsKeyId: props.ebsEncryptionKmsKey?.keyId, + }, + }); + + /** + * Singleton pattern to define the log group for the singleton function + * in the stack + */ + const stack = cdk.Stack.of(scope); + const logGroup = + (stack.node.tryFindChild(`${provider.node.id}LogGroup`) as cdk.aws_logs.LogGroup) ?? + new cdk.aws_logs.LogGroup(stack, `${provider.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${(provider.node.findChild('Handler') as cdk.aws_lambda.CfnFunction).ref}`, + retention: props.logRetentionInDays, + encryptionKey: props.logGroupKmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + resource.node.addDependency(logGroup); + + this.id = resource.ref; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ec2/get-transit-gateway-attachment/index.ts b/source/packages/@aws-accelerator/constructs/lib/aws-ec2/get-transit-gateway-attachment/index.ts new file mode 100644 index 000000000..05624ad24 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ec2/get-transit-gateway-attachment/index.ts @@ -0,0 +1,88 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as AWS from 'aws-sdk'; + +import { throttlingBackOff } from '@aws-accelerator/utils'; + +AWS.config.logger = console; + +/** + * get-transit-gateway-attachment - lambda handler + * + * @param event + * @returns + */ +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + PhysicalResourceId: string | undefined; + Status: string; + } + | undefined +> { + const name = event.ResourceProperties['name']; + const transitGatewayId = event.ResourceProperties['transitGatewayId']; + const roleArn = event.ResourceProperties['roleArn']; + + switch (event.RequestType) { + case 'Create': + case 'Update': + const stsClient = new AWS.STS({}); + + const assumeRoleResponse = await throttlingBackOff(() => + stsClient + .assumeRole({ + RoleArn: roleArn, + RoleSessionName: 'GetTransitGatewayAttachmentSession', + }) + .promise(), + ); + + const ec2Client = new AWS.EC2({ + credentials: { + accessKeyId: assumeRoleResponse.Credentials?.AccessKeyId ?? '', + secretAccessKey: assumeRoleResponse.Credentials?.SecretAccessKey ?? '', + sessionToken: assumeRoleResponse.Credentials?.SessionToken, + }, + }); + + let nextToken: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => + ec2Client.describeTransitGatewayAttachments({ NextToken: nextToken }).promise(), + ); + for (const attachment of page.TransitGatewayAttachments ?? []) { + if (attachment.TransitGatewayId === transitGatewayId && attachment.State === 'available') { + const nameTag = attachment.Tags?.find(t => t.Key === 'Name'); + if (nameTag && nameTag.Value === name) { + console.log(attachment); + return { + PhysicalResourceId: attachment.TransitGatewayAttachmentId, + Status: 'SUCCESS', + }; + } + } + } + nextToken = page.NextToken; + } while (nextToken); + + throw new Error(`Attachment ${name} for ${transitGatewayId} not found`); + + case 'Delete': + // Do Nothing + return { + PhysicalResourceId: event.PhysicalResourceId, + Status: 'SUCCESS', + }; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ec2/get-transit-gateway-attachment/package.json b/source/packages/@aws-accelerator/constructs/lib/aws-ec2/get-transit-gateway-attachment/package.json new file mode 100644 index 000000000..e00543437 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ec2/get-transit-gateway-attachment/package.json @@ -0,0 +1,46 @@ +{ + "name": "@aws-accelerator/constructs-aws-ec2-get-transit-gateway-attachment", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --outfile=./dist/index.js --platform=node --target=node14 --external:aws-sdk index.ts", + "test": "", + "testreport": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "esbuild": "0.14.2", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2" + }, + "peerDependencies": { + "eslint": "8.4.1" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1001.0" + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ec2/get-transit-gateway-attachment/tsconfig.json b/source/packages/@aws-accelerator/constructs/lib/aws-ec2/get-transit-gateway-attachment/tsconfig.json new file mode 100644 index 000000000..ebcb98a79 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ec2/get-transit-gateway-attachment/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ec2/prefix-list.ts b/source/packages/@aws-accelerator/constructs/lib/aws-ec2/prefix-list.ts new file mode 100644 index 000000000..d8a4c487e --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ec2/prefix-list.ts @@ -0,0 +1,77 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { CfnTag, IResource, Resource } from 'aws-cdk-lib'; +import * as ec2 from 'aws-cdk-lib/aws-ec2'; +import { CfnPrefixList } from 'aws-cdk-lib/aws-ec2'; +import { Construct } from 'constructs'; + +export interface IPrefixList extends IResource { + /** + * The ID of the prefix list. + */ + readonly prefixListId: string; +} + +export interface PrefixListProps { + /** + * The name of the prefix list. + */ + readonly name: string; + + /** + * IP Address Family IPv4 or IPv6. + * @default -- 'IPv4' + */ + readonly addressFamily: string; + + /** + * Maxinum number of CIDR block entries. + * + * @default -- 1 + */ + readonly maxEntries: number; + + /** + * List of CIDR block entries. + * + */ + readonly entries: string[]; + + /** + * Any tags assigned to the prefix list. + */ + readonly tags?: CfnTag[]; +} + +export class PrefixList extends Resource implements IPrefixList { + public readonly prefixListId: string; + + constructor(scope: Construct, id: string, props: PrefixListProps) { + super(scope, id); + + const cidrBlocks: CfnPrefixList.EntryProperty[] = props.entries.map(item => { + return { cidr: item }; + }); + + const resource = new ec2.CfnPrefixList(this, 'Resource', { + prefixListName: props.name, + addressFamily: props.addressFamily, + maxEntries: props.maxEntries, + entries: cidrBlocks, + tags: props.tags, + }); + + this.prefixListId = resource.attrPrefixListId; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ec2/route-table.ts b/source/packages/@aws-accelerator/constructs/lib/aws-ec2/route-table.ts new file mode 100644 index 000000000..611579529 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ec2/route-table.ts @@ -0,0 +1,101 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +import { Vpc } from './vpc'; + +export interface IRouteTable extends cdk.IResource { + /** + * The identifier of the route table + * + * @attribute + */ + readonly routeTableId: string; + + /** + * The VPC associated with the route table + * + * @attribute + */ + readonly vpc: Vpc; +} + +export interface RouteTableProps { + readonly name: string; + readonly vpc: Vpc; + readonly tags?: cdk.CfnTag[]; +} + +export class RouteTable extends cdk.Resource implements IRouteTable { + public readonly routeTableId: string; + + public readonly vpc: Vpc; + + constructor(scope: Construct, id: string, props: RouteTableProps) { + super(scope, id); + + this.vpc = props.vpc; + + const resource = new cdk.aws_ec2.CfnRouteTable(this, 'Resource', { + vpcId: props.vpc.vpcId, + tags: props.tags, + }); + cdk.Tags.of(this).add('Name', props.name); + + this.routeTableId = resource.ref; + } + + public addTransitGatewayRoute( + id: string, + destination: string, + transitGatewayId: string, + transitGatewayAttachment: cdk.CfnResource, + ): void { + const route = new cdk.aws_ec2.CfnRoute(this, id, { + routeTableId: this.routeTableId, + destinationCidrBlock: destination, + transitGatewayId: transitGatewayId, + }); + route.addDependsOn(transitGatewayAttachment); + } + + public addNatGatewayRoute(id: string, destination: string, natGatewayId: string): void { + new cdk.aws_ec2.CfnRoute(this, id, { + routeTableId: this.routeTableId, + destinationCidrBlock: destination, + natGatewayId: natGatewayId, + }); + } + + public addInternetGatewayRoute(id: string, destination: string): void { + if (!this.vpc.internetGateway) { + throw new Error('Attempting to add Internet Gateway route without an IGW defined.'); + } + + if (!this.vpc.internetGatewayAttachment) { + throw new Error('Attempting to add Internet Gateway route without an IGW attached.'); + } + + const route = new cdk.aws_ec2.CfnRoute(this, id, { + routeTableId: this.routeTableId, + destinationCidrBlock: destination, + gatewayId: this.vpc.internetGateway.ref, + }); + + // Need to add depends on for the attachment, as IGW needs to be part of + // the network (vpc) + route.addDependsOn(this.vpc.internetGatewayAttachment); + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ec2/transit-gateway-route-table.ts b/source/packages/@aws-accelerator/constructs/lib/aws-ec2/transit-gateway-route-table.ts new file mode 100644 index 000000000..a5f32a699 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ec2/transit-gateway-route-table.ts @@ -0,0 +1,53 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as ec2 from 'aws-cdk-lib/aws-ec2'; +import * as cdk from 'aws-cdk-lib'; +import { pascalCase } from 'change-case'; +import { Construct } from 'constructs'; + +export interface TransitGatewayRouteTableProps { + /** + * The name of the transit gateway route table. Will be assigned to the Name tag + */ + readonly name: string; + + /** + * The ID of the transit gateway. + */ + readonly transitGatewayId: string; + + /** + * The tags that will be attached to the transit gateway route table. + */ + readonly tags?: cdk.CfnTag[]; +} + +/** + * Creates a Transit Gateway Route Table + */ +export class TransitGatewayRouteTable extends Construct { + public readonly id: string; + + constructor(scope: Construct, id: string, props: TransitGatewayRouteTableProps) { + super(scope, id); + + const routeTable = new ec2.CfnTransitGatewayRouteTable(this, pascalCase(`${props.name}TransitGatewayRouteTable`), { + transitGatewayId: props.transitGatewayId, + tags: props.tags, + }); + cdk.Tags.of(this).add('Name', props.name); + + this.id = routeTable.ref; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ec2/transit-gateway-static-route.ts b/source/packages/@aws-accelerator/constructs/lib/aws-ec2/transit-gateway-static-route.ts new file mode 100644 index 000000000..28592f1a1 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ec2/transit-gateway-static-route.ts @@ -0,0 +1,54 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as ec2 from 'aws-cdk-lib/aws-ec2'; +import { Construct } from 'constructs'; + +export interface TransitGatewayStaticRouteProps { + /** + * The ID of the transit gateway route table. + */ + readonly transitGatewayRouteTableId: string; + + /** + * Determines if route is blackholed. + */ + readonly blackhole?: boolean; + + /** + * The CIDR block for the route. + * + */ + readonly destinationCidrBlock?: string; + + /** + * The identifier of the Transit Gateway Attachment + * + */ + readonly transitGatewayAttachmentId?: string; +} + +/** + * Creates a Transit Gateway Static Route + */ +export class TransitGatewayStaticRoute extends Construct { + constructor(scope: Construct, id: string, props: TransitGatewayStaticRouteProps) { + super(scope, id); + new ec2.CfnTransitGatewayRoute(this, 'StaticRoute', { + transitGatewayRouteTableId: props.transitGatewayRouteTableId, + blackhole: props.blackhole, + destinationCidrBlock: props.destinationCidrBlock, + transitGatewayAttachmentId: props.transitGatewayAttachmentId, + }); + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ec2/transit-gateway.ts b/source/packages/@aws-accelerator/constructs/lib/aws-ec2/transit-gateway.ts new file mode 100644 index 000000000..9b9bfd38c --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ec2/transit-gateway.ts @@ -0,0 +1,320 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import { v4 as uuidv4 } from 'uuid'; + +import { TransitGatewayAttachmentOptionsConfig } from '@aws-accelerator/config'; + +const path = require('path'); + +export interface ITransitGatewayRouteTableAssociation extends cdk.IResource { + readonly transitGatewayAttachmentId: string; // TODO: change to ITransitGatewayAttachment + readonly transitGatewayRouteTableId: string; // TODO: change to ITransitGatewayRouteTable +} + +export interface TransitGatewayRouteTableAssociationProps { + readonly transitGatewayAttachmentId: string; + readonly transitGatewayRouteTableId: string; +} + +export class TransitGatewayRouteTableAssociation extends cdk.Resource implements ITransitGatewayRouteTableAssociation { + readonly transitGatewayAttachmentId: string; + readonly transitGatewayRouteTableId: string; + + constructor(scope: Construct, id: string, props: TransitGatewayRouteTableAssociationProps) { + super(scope, id); + + this.transitGatewayAttachmentId = props.transitGatewayAttachmentId; + this.transitGatewayRouteTableId = props.transitGatewayRouteTableId; + + new cdk.aws_ec2.CfnTransitGatewayRouteTableAssociation(this, 'Resource', { + transitGatewayAttachmentId: props.transitGatewayAttachmentId, + transitGatewayRouteTableId: props.transitGatewayRouteTableId, + }); + } +} + +export interface ITransitGatewayRouteTablePropagation extends cdk.IResource { + readonly transitGatewayAttachmentId: string; // TODO: change to ITransitGatewayAttachment + readonly transitGatewayRouteTableId: string; // TODO: change to ITransitGatewayRouteTable +} + +export interface TransitGatewayRouteTablePropagationProps { + readonly transitGatewayAttachmentId: string; + readonly transitGatewayRouteTableId: string; +} + +export class TransitGatewayRouteTablePropagation extends cdk.Resource implements ITransitGatewayRouteTablePropagation { + readonly transitGatewayAttachmentId: string; + readonly transitGatewayRouteTableId: string; + + constructor(scope: Construct, id: string, props: TransitGatewayRouteTablePropagationProps) { + super(scope, id); + + this.transitGatewayAttachmentId = props.transitGatewayAttachmentId; + this.transitGatewayRouteTableId = props.transitGatewayRouteTableId; + + new cdk.aws_ec2.CfnTransitGatewayRouteTablePropagation(this, 'Resource', { + transitGatewayAttachmentId: props.transitGatewayAttachmentId, + transitGatewayRouteTableId: props.transitGatewayRouteTableId, + }); + } +} + +export interface ITransitGatewayAttachment extends cdk.IResource { + readonly transitGatewayAttachmentId: string; + readonly transitGatewayAttachmentName: string; +} + +export interface TransitGatewayAttachmentProps { + readonly name: string; + readonly partition: string; + readonly transitGatewayId: string; + readonly subnetIds: string[]; + readonly vpcId: string; + readonly options?: TransitGatewayAttachmentOptionsConfig; + readonly tags?: cdk.CfnTag[]; +} + +export interface TransitGatewayAttachmentLookupOptions { + readonly name: string; + readonly owningAccountId: string; + readonly transitGatewayId: string; // TODO: change to ITransitGateway + readonly roleName?: string; + /** + * Custom resource lambda log group encryption key + */ + readonly kmsKey: cdk.aws_kms.Key; + /** + * Custom resource lambda log retention in days + */ + readonly logRetentionInDays: number; +} + +export class TransitGatewayAttachment extends cdk.Resource implements ITransitGatewayAttachment { + public static fromLookup( + scope: Construct, + id: string, + options: TransitGatewayAttachmentLookupOptions, + ): ITransitGatewayAttachment { + class Import extends cdk.Resource implements ITransitGatewayAttachment { + public readonly transitGatewayAttachmentId: string; + public readonly transitGatewayAttachmentName = options.name; + + constructor(scope: Construct, id: string) { + super(scope, id); + + const GET_TRANSIT_GATEWAY_ATTACHMENT = 'Custom::GetTransitGatewayAttachment'; + + const provider = cdk.CustomResourceProvider.getOrCreateProvider(this, GET_TRANSIT_GATEWAY_ATTACHMENT, { + codeDirectory: path.join(__dirname, 'get-transit-gateway-attachment/dist'), + runtime: cdk.CustomResourceProviderRuntime.NODEJS_14_X, + policyStatements: [ + { + Effect: 'Allow', + Action: ['sts:AssumeRole'], + Resource: '*', + }, + ], + }); + + const resource = new cdk.CustomResource(this, 'Resource', { + resourceType: GET_TRANSIT_GATEWAY_ATTACHMENT, + serviceToken: provider.serviceToken, + properties: { + name: options.name, + transitGatewayId: options.transitGatewayId, + roleArn: cdk.Stack.of(this).formatArn({ + service: 'iam', + region: '', + account: options.owningAccountId, + resource: 'role', + arnFormat: cdk.ArnFormat.SLASH_RESOURCE_NAME, + resourceName: options.roleName, + }), + uuid: uuidv4(), // Generates a new UUID to force the resource to update + }, + }); + + /** + * Singleton pattern to define the log group for the singleton function + * in the stack + */ + const stack = cdk.Stack.of(scope); + const logGroup = + (stack.node.tryFindChild(`${provider.node.id}LogGroup`) as cdk.aws_logs.LogGroup) ?? + new cdk.aws_logs.LogGroup(stack, `${provider.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${(provider.node.findChild('Handler') as cdk.aws_lambda.CfnFunction).ref}`, + retention: options.logRetentionInDays, + encryptionKey: options.kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + resource.node.addDependency(logGroup); + + this.transitGatewayAttachmentId = resource.ref; + } + } + return new Import(scope, id); + } + + public readonly transitGatewayAttachmentId: string; + public readonly transitGatewayAttachmentName: string; + + constructor(scope: Construct, id: string, props: TransitGatewayAttachmentProps) { + super(scope, id); + + let resource: cdk.aws_ec2.CfnTransitGatewayVpcAttachment | cdk.aws_ec2.CfnTransitGatewayAttachment; + if (props.partition === 'aws') { + resource = new cdk.aws_ec2.CfnTransitGatewayVpcAttachment(this, 'Resource', { + vpcId: props.vpcId, + transitGatewayId: props.transitGatewayId, + subnetIds: props.subnetIds, + options: { + ApplianceModeSupport: props.options?.applianceModeSupport ?? 'disable', + DnsSupport: props.options?.dnsSupport ?? 'enable', + Ipv6Support: props.options?.ipv6Support ?? 'disable', + }, + tags: props.tags, + }); + } else { + resource = new cdk.aws_ec2.CfnTransitGatewayAttachment(this, 'Resource', { + vpcId: props.vpcId, + transitGatewayId: props.transitGatewayId, + subnetIds: props.subnetIds, + tags: props.tags, + }); + } + // Add name tag + cdk.Tags.of(this).add('Name', props.name); + + this.transitGatewayAttachmentId = resource.ref; + this.transitGatewayAttachmentName = props.name; + } +} + +export interface ITransitGateway extends cdk.IResource { + /** + * The identifier of the transit gateway + * + * @attribute + */ + readonly transitGatewayId: string; + + /** + * The name of the transit gateway + * + * @attribute + */ + readonly transitGatewayName: string; + + /** + * The ARN of the transit gateway + * + * @attribute + */ + readonly transitGatewayArn: string; +} + +export interface TransitGatewayProps { + /** + * The name of the transit gateway. Will be assigned to the Name tag + */ + readonly name: string; + + /** + * A private Autonomous System Number (ASN) for the Amazon side of a BGP session. The range is + * 64512 to 65534 for 16-bit ASNs. The default is 64512. + */ + readonly amazonSideAsn?: number; + + /** + * Enable or disable automatic acceptance of attachment requests. Disabled by default. + */ + readonly autoAcceptSharedAttachments?: string; + + /** + * Enable or disable automatic association with the default association route table. Enabled by + * default. + */ + readonly defaultRouteTableAssociation?: string; + + /** + * Enable or disable automatic propagation of routes to the default propagation route table. + * Enabled by default. + */ + readonly defaultRouteTablePropagation?: string; + + /** + * The description of the transit gateway. + */ + readonly description?: string; + + /** + * Enable or disable DNS support. Enabled by default. + */ + readonly dnsSupport?: string; + + /** + * Indicates whether multicast is enabled on the transit gateway + */ + readonly multicastSupport?: string; + + /** + * Enable or disable Equal Cost Multipath Protocol support. Enabled by default. + */ + readonly vpnEcmpSupport?: string; + + /** + * Tags that will be attached to the transit gateway + */ + readonly tags?: cdk.CfnTag[]; +} + +/** + * Creates a Transit Gateway + */ +export class TransitGateway extends cdk.Resource implements ITransitGateway { + readonly transitGatewayId: string; + + readonly transitGatewayName: string; + + readonly transitGatewayArn: string; + + constructor(scope: Construct, id: string, props: TransitGatewayProps) { + super(scope, id); + + const resource = new cdk.aws_ec2.CfnTransitGateway(this, 'Resource', { + amazonSideAsn: props.amazonSideAsn, + autoAcceptSharedAttachments: props.autoAcceptSharedAttachments, + defaultRouteTableAssociation: props.defaultRouteTableAssociation, + defaultRouteTablePropagation: props.defaultRouteTablePropagation, + dnsSupport: props.dnsSupport, + vpnEcmpSupport: props.vpnEcmpSupport, + tags: props.tags, + }); + cdk.Tags.of(this).add('Name', props.name); + + this.transitGatewayId = resource.ref; + + this.transitGatewayName = props.name; + + this.transitGatewayArn = cdk.Stack.of(this).formatArn({ + service: 'ec2', + resource: 'transit-gateway', + arnFormat: cdk.ArnFormat.SLASH_RESOURCE_NAME, + resourceName: this.transitGatewayId, + }); + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ec2/vpc-endpoint.ts b/source/packages/@aws-accelerator/constructs/lib/aws-ec2/vpc-endpoint.ts new file mode 100644 index 000000000..021e33c74 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ec2/vpc-endpoint.ts @@ -0,0 +1,99 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +import { ISecurityGroup } from './vpc'; + +export interface IVpcEndpoint extends cdk.IResource { + readonly vpcEndpointId: string; + readonly service: string; + readonly vpcId: string; + readonly dnsName?: string; + readonly hostedZoneId?: string; +} + +export interface VpcEndpointProps { + readonly vpcEndpointType: cdk.aws_ec2.VpcEndpointType; + readonly service: string; + readonly vpcId: string; + readonly subnets?: string[]; + readonly securityGroups?: ISecurityGroup[]; + readonly privateDnsEnabled?: boolean; + readonly policyDocument?: cdk.aws_iam.PolicyDocument; + readonly routeTables?: string[]; +} + +export class VpcEndpoint extends cdk.Resource implements IVpcEndpoint { + public readonly vpcEndpointId: string; + public readonly vpcId: string; + public readonly service: string; + public readonly dnsName?: string; + public readonly hostedZoneId?: string; + + constructor(scope: Construct, id: string, props: VpcEndpointProps) { + super(scope, id); + + this.service = props.service; + this.vpcId = props.vpcId; + + // Add constant for sagemaker conditionals + const sagemakerArray = ['notebook', 'studio']; + + if (props.vpcEndpointType === cdk.aws_ec2.VpcEndpointType.INTERFACE) { + let serviceName = `com.amazonaws.${cdk.Stack.of(this).region}.${props.service}`; + if (sagemakerArray.includes(this.service)) { + serviceName = `aws.sagemaker.${cdk.Stack.of(this).region}.${props.service}`; + } + if (this.service === 's3-global.accesspoint') { + serviceName = `com.aws.${props.service}`; + } + + const resource = new cdk.aws_ec2.CfnVPCEndpoint(this, 'Resource', { + serviceName, + vpcEndpointType: props.vpcEndpointType, + vpcId: this.vpcId, + subnetIds: props.subnets, + securityGroupIds: props.securityGroups?.map(item => item.securityGroupId), + privateDnsEnabled: props.privateDnsEnabled, + policyDocument: props.policyDocument, + }); + this.vpcEndpointId = resource.ref; + + let dnsEntriesIndex = 0; + if (sagemakerArray.includes(this.service)) { + // TODO Top 3 DNS names are not valid so selecting the 4th DNS + // need to find a better way to identify the valid DNS for PHZ + dnsEntriesIndex = 4; + } + + this.dnsName = cdk.Fn.select(1, cdk.Fn.split(':', cdk.Fn.select(dnsEntriesIndex, resource.attrDnsEntries))); + this.hostedZoneId = cdk.Fn.select(0, cdk.Fn.split(':', cdk.Fn.select(dnsEntriesIndex, resource.attrDnsEntries))); + return; + } + + if (props.vpcEndpointType === cdk.aws_ec2.VpcEndpointType.GATEWAY) { + const resource = new cdk.aws_ec2.CfnVPCEndpoint(this, 'Resource', { + serviceName: new cdk.aws_ec2.GatewayVpcEndpointAwsService(props.service).name, + vpcId: this.vpcId, + routeTableIds: props.routeTables, + policyDocument: props.policyDocument, + }); + this.vpcEndpointId = resource.ref; + return; + } + + throw new Error('Invalid vpcEndpointType specified'); + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ec2/vpc-peering.ts b/source/packages/@aws-accelerator/constructs/lib/aws-ec2/vpc-peering.ts new file mode 100644 index 000000000..1b14595df --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ec2/vpc-peering.ts @@ -0,0 +1,94 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +export interface IVpcPeering extends cdk.IResource { + /** + * The name of the peering connection. + */ + readonly name: string; + + /** + * The ID of the VPC peering connection. + */ + readonly peeringId: string; +} + +export interface VpcPeeringProps { + /** + * The name of the peering connection. + */ + readonly name: string; + /** + * The AWS account ID of the owner of the accepter VPC. + */ + readonly peerOwnerId: string; + + /** + * The Region code for the accepter VPC, if the accepter VPC is + * located in a Region other than the Region in which you make the request. + */ + readonly peerRegion: string; + /** + * The ID of the VPC with which you are creating the VPC peering connection. + */ + readonly peerVpcId: string; + + /** + * The ID of the VPC creating the connection request. + */ + readonly vpcId: string; + + /** + * The name of the VPC peer role for the + * peering connection in another AWS account. + */ + readonly peerRoleName?: string; + + /** + * An optional list of CloudFormation tags. + */ + readonly tags?: cdk.CfnTag[]; +} + +export class VpcPeering extends cdk.Resource implements IVpcPeering { + public readonly name: string; + public readonly peeringId: string; + private roleArn?: string; + + constructor(scope: Construct, id: string, props: VpcPeeringProps) { + super(scope, id); + + // Set name tag + this.name = props.name; + props.tags?.push({ key: 'Name', value: this.name }); + + // Set role ARN + if (props.peerRoleName) { + this.roleArn = `arn:${cdk.Stack.of(this).partition}:iam::${props.peerOwnerId}:role/${props.peerRoleName}`; + } + + const resource = new cdk.aws_ec2.CfnVPCPeeringConnection(this, 'Resource', { + peerOwnerId: props.peerOwnerId, + peerRegion: props.peerRegion, + peerVpcId: props.peerVpcId, + vpcId: props.vpcId, + peerRoleArn: this.roleArn, + tags: props.tags, + }); + + this.peeringId = resource.ref; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ec2/vpc.ts b/source/packages/@aws-accelerator/constructs/lib/aws-ec2/vpc.ts new file mode 100644 index 000000000..dc8fa6650 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ec2/vpc.ts @@ -0,0 +1,545 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +import { IPrefixList } from './prefix-list'; +import { IRouteTable } from './route-table'; + +export interface ISubnet extends cdk.IResource { + /** + * The identifier of the subnet + * + * @attribute + */ + readonly subnetId: string; + + /** + * The identifier of the subnet + * + * @attribute + */ + readonly subnetArn: string; + + /** + * The name of the subnet + * + * @attribute + */ + readonly subnetName: string; + + /** + * The Availability Zone the subnet is located in + * + * @attribute + */ + readonly availabilityZone: string; +} + +export interface SubnetProps { + readonly name: string; + readonly availabilityZone: string; + readonly ipv4CidrBlock: string; + readonly mapPublicIpOnLaunch?: boolean; + readonly routeTable: IRouteTable; + readonly vpc: IVpc; + readonly tags?: cdk.CfnTag[]; + // readonly nacl: INacl; +} + +export class Subnet extends cdk.Resource implements ISubnet { + public readonly subnetName: string; + public readonly availabilityZone: string; + public readonly ipv4CidrBlock: string; + public readonly mapPublicIpOnLaunch?: boolean; + public readonly routeTable: IRouteTable; + public readonly subnetId: string; + public readonly subnetArn: string; + + constructor(scope: Construct, id: string, props: SubnetProps) { + super(scope, id); + + this.subnetName = props.name; + this.availabilityZone = props.availabilityZone; + this.ipv4CidrBlock = props.ipv4CidrBlock; + this.mapPublicIpOnLaunch = props.mapPublicIpOnLaunch; + this.routeTable = props.routeTable; + + //props.tags?.push({ key: 'Name', value: props.name }); + + const resource = new cdk.aws_ec2.CfnSubnet(this, 'Resource', { + vpcId: props.vpc.vpcId, + cidrBlock: props.ipv4CidrBlock, + availabilityZone: props.availabilityZone, + mapPublicIpOnLaunch: props.mapPublicIpOnLaunch, + tags: props.tags, + }); + + cdk.Tags.of(this).add('Name', props.name); + + this.subnetId = resource.ref; + this.subnetArn = cdk.Stack.of(this).formatArn({ + service: 'ec2', + resource: 'subnet', + arnFormat: cdk.ArnFormat.SLASH_RESOURCE_NAME, + resourceName: resource.ref, + }); + + new cdk.aws_ec2.CfnSubnetRouteTableAssociation(this, 'RouteTableAssociation', { + subnetId: this.subnetId, + routeTableId: props.routeTable.routeTableId, + }); + } +} + +export interface INatGateway extends cdk.IResource { + /** + * The identifier of the NAT Gateway + * + * @attribute + */ + readonly natGatewayId: string; + + /** + * The name of the NAT Gateway + * + * @attribute + */ + readonly natGatewayName: string; +} + +export interface NatGatewayProps { + readonly name: string; + readonly subnet: ISubnet; + readonly tags?: cdk.CfnTag[]; +} + +export class NatGateway extends cdk.Resource implements INatGateway { + public readonly natGatewayId: string; + public readonly natGatewayName: string; + + constructor(scope: Construct, id: string, props: NatGatewayProps) { + super(scope, id); + + this.natGatewayName = props.name; + + const resource = new cdk.aws_ec2.CfnNatGateway(this, 'Resource', { + subnetId: props.subnet.subnetId, + allocationId: new cdk.aws_ec2.CfnEIP(this, 'Eip', { + domain: 'vpc', + }).attrAllocationId, + tags: props.tags, + }); + cdk.Tags.of(this).add('Name', props.name); + + this.natGatewayId = resource.ref; + } +} + +export interface ISecurityGroup extends cdk.IResource { + /** + * ID for the current security group + * @attribute + */ + readonly securityGroupId: string; +} + +export interface SecurityGroupProps { + /** + * The name of the security group. For valid values, see the GroupName + * parameter of the CreateSecurityGroup action in the Amazon EC2 API + * Reference. + * + * It is not recommended to use an explicit group name. + * + * @default If you don't specify a GroupName, AWS CloudFormation generates a + * unique physical ID and uses that ID for the group name. + */ + readonly securityGroupName?: string; + + /** + * A description of the security group. + * + * @default The default name will be the construct's CDK path. + */ + readonly description?: string; + + /** + * The outbound rules associated with the security group. + */ + readonly securityGroupEgress?: SecurityGroupEgressRuleProps[]; + + /** + * The inbound rules associated with the security group. + */ + readonly securityGroupIngress?: SecurityGroupIngressRuleProps[]; + + /** + * The tags that will be attached to the security group + */ + readonly tags?: cdk.CfnTag[]; + + /** + * The VPC in which to create the security group. + */ + readonly vpc?: IVpc; + + /** + * The VPC in which to create the security group. + */ + readonly vpcId?: string; +} + +export interface SecurityGroupIngressRuleProps { + readonly ipProtocol: string; + readonly description?: string; + readonly cidrIp?: string; + readonly cidrIpv6?: string; + readonly sourcePrefixList?: IPrefixList; + readonly sourcePrefixListId?: string; + readonly sourceSecurityGroup?: ISecurityGroup; + readonly fromPort?: number; + readonly toPort?: number; +} + +export interface SecurityGroupEgressRuleProps { + readonly ipProtocol: string; + readonly description?: string; + readonly cidrIp?: string; + readonly cidrIpv6?: string; + readonly destinationPrefixList?: IPrefixList; + readonly destinationPrefixListId?: string; + readonly destinationSecurityGroup?: ISecurityGroup; + readonly fromPort?: number; + readonly toPort?: number; +} + +export class SecurityGroup extends cdk.Resource implements ISecurityGroup { + public readonly securityGroupId: string; + + private readonly securityGroup: cdk.aws_ec2.CfnSecurityGroup; + + constructor(scope: Construct, id: string, props: SecurityGroupProps) { + super(scope, id); + + if (!props.vpc?.vpcId && !props.vpcId) { + throw new Error(`A property value for vpc or vpcId must be specified`); + } + + this.securityGroup = new cdk.aws_ec2.CfnSecurityGroup(this, 'Resource', { + groupDescription: props.description ?? '', + securityGroupEgress: props.securityGroupEgress, + securityGroupIngress: props.securityGroupIngress, + groupName: props.securityGroupName, + vpcId: props.vpc?.vpcId ?? props.vpcId, + tags: props.tags, + }); + + if (props.securityGroupName) { + cdk.Tags.of(this.securityGroup).add('Name', props.securityGroupName); + } + + this.securityGroupId = this.securityGroup.ref; + } + + public addIngressRule(id: string, props: SecurityGroupIngressRuleProps) { + new cdk.aws_ec2.CfnSecurityGroupIngress(this, id, { + groupId: this.securityGroupId, + ipProtocol: props.ipProtocol, + description: props.description, + cidrIp: props.cidrIp, + cidrIpv6: props.cidrIpv6, + sourcePrefixListId: props.sourcePrefixList?.prefixListId, + sourceSecurityGroupId: props.sourceSecurityGroup?.securityGroupId, + fromPort: props.fromPort, + toPort: props.toPort, + }); + } + + public addEgressRule(id: string, props: SecurityGroupEgressRuleProps) { + new cdk.aws_ec2.CfnSecurityGroupEgress(this, id, { + groupId: this.securityGroupId, + ipProtocol: props.ipProtocol, + description: props.description, + cidrIp: props.cidrIp, + cidrIpv6: props.cidrIpv6, + destinationPrefixListId: props.destinationPrefixList?.prefixListId, + destinationSecurityGroupId: props.destinationSecurityGroup?.securityGroupId, + fromPort: props.fromPort, + toPort: props.toPort, + }); + } +} + +/** + * A NetworkAcl + * + * + */ +export interface INetworkAcl extends cdk.IResource { + /** + * ID for the current Network ACL + * @attribute + */ + readonly networkAclId: string; + + /** + * ID for the current Network ACL + * @attribute + */ + readonly networkAclName: string; +} + +/** + * A NetworkAclBase that is not created in this template + */ +abstract class NetworkAclBase extends cdk.Resource implements INetworkAcl { + public abstract readonly networkAclId: string; + public abstract readonly networkAclName: string; +} + +/** + * Properties to create NetworkAcl + */ +export interface NetworkAclProps { + /** + * The name of the NetworkAcl. + */ + readonly networkAclName: string; + + /** + * The VPC in which to create the NetworkACL. + */ + readonly vpc: IVpc; + + /** + * The tags which will be attached to the NetworkACL. + */ + readonly tags?: cdk.CfnTag[]; +} + +/** + * Define a new custom network ACL + * + * By default, will deny all inbound and outbound traffic unless entries are + * added explicitly allowing it. + */ +export class NetworkAcl extends NetworkAclBase { + /** + * The ID of the NetworkACL + * + * @attribute + */ + public readonly networkAclId: string; + + /** + * The Name of the NetworkACL + * + * @attribute + */ + public readonly networkAclName: string; + + /** + * The VPC ID for this NetworkACL + * + * @attribute + */ + public readonly networkAclVpcId: string; + + constructor(scope: Construct, id: string, props: NetworkAclProps) { + super(scope, id); + + this.networkAclName = props.networkAclName; + + const resource = new cdk.aws_ec2.CfnNetworkAcl(this, 'Resource', { + vpcId: props.vpc.vpcId, + tags: props.tags, + }); + cdk.Tags.of(this).add('Name', props.networkAclName); + + this.networkAclId = resource.ref; + this.networkAclVpcId = resource.vpcId; + } + + public associateSubnet(id: string, props: { subnet: ISubnet }) { + new cdk.aws_ec2.CfnSubnetNetworkAclAssociation(this, id, { + networkAclId: this.networkAclId, + subnetId: props.subnet.subnetId, + }); + } + + public addEntry( + id: string, + props: { + ruleNumber: number; + protocol: number; + ruleAction: 'allow' | 'deny'; + egress: boolean; + cidrBlock?: string; + icmp?: { + code?: number; + type?: number; + }; + ipv6CidrBlock?: string; + portRange?: { + from: number; + to: number; + }; + }, + ) { + new cdk.aws_ec2.CfnNetworkAclEntry(this, id, { + networkAclId: this.networkAclId, + ...props, + }); + } +} + +export interface IVpc extends cdk.IResource { + /** + * The identifier of the vpc + * + * @attribute + */ + readonly vpcId: string; +} + +/** + * Construction properties for a VPC object. + */ +export interface VpcProps { + readonly name: string; + readonly ipv4CidrBlock: string; + readonly dhcpOptions?: string; + readonly enableDnsHostnames?: boolean; + readonly enableDnsSupport?: boolean; + readonly instanceTenancy?: 'default' | 'dedicated'; + readonly internetGateway?: boolean; + readonly tags?: cdk.CfnTag[]; +} + +/** + * Defines a VPC object + */ +export class Vpc extends cdk.Resource implements IVpc { + public readonly vpcId: string; + public readonly internetGateway: cdk.aws_ec2.CfnInternetGateway | undefined; + public readonly internetGatewayAttachment: cdk.aws_ec2.CfnVPCGatewayAttachment | undefined; + public readonly dhcpOptionsAssociation: cdk.aws_ec2.CfnVPCDHCPOptionsAssociation | undefined; + + constructor(scope: Construct, id: string, props: VpcProps) { + super(scope, id); + + const resource = new cdk.aws_ec2.CfnVPC(this, 'Resource', { + cidrBlock: props.ipv4CidrBlock, + enableDnsHostnames: props.enableDnsHostnames, + enableDnsSupport: props.enableDnsSupport, + instanceTenancy: props.instanceTenancy, + tags: props.tags, + }); + cdk.Tags.of(this).add('Name', props.name); + + this.vpcId = resource.ref; + + if (props.internetGateway) { + this.internetGateway = new cdk.aws_ec2.CfnInternetGateway(this, 'InternetGateway', {}); + + this.internetGatewayAttachment = new cdk.aws_ec2.CfnVPCGatewayAttachment(this, 'InternetGatewayAttachment', { + internetGatewayId: this.internetGateway.ref, + vpcId: this.vpcId, + }); + } + + if (props.dhcpOptions) { + this.dhcpOptionsAssociation = new cdk.aws_ec2.CfnVPCDHCPOptionsAssociation(this, 'DhcpOptionsAssociation', { + dhcpOptionsId: props.dhcpOptions, + vpcId: this.vpcId, + }); + } + } + + public addFlowLogs(options: { + destinations: ('s3' | 'cloud-watch-logs')[]; + trafficType: 'ALL' | 'REJECT' | 'ACCEPT'; + maxAggregationInterval: number; + logFormat?: string; + logRetentionInDays?: number; + encryptionKey?: cdk.aws_kms.IKey | undefined; + bucketArn?: string; + }) { + // Validate maxAggregationInterval + const maxAggregationInterval = options.maxAggregationInterval; + if (maxAggregationInterval != 60 && maxAggregationInterval != 600) { + throw new Error(`Invalid maxAggregationInterval (${maxAggregationInterval}) - must be 60 or 600 seconds`); + } + + // Destination: CloudWatch Logs + if (options.destinations.includes('cloud-watch-logs')) { + if (!options.encryptionKey) { + throw new Error('encryptionKey not provided for cwl flow log'); + } + + if (!options.logRetentionInDays) { + throw new Error('logRetentionInDays not provided for cwl flow log'); + } + + const logGroup = new cdk.aws_logs.LogGroup(this, 'FlowLogsGroup', { + encryptionKey: options.encryptionKey, + retention: options.logRetentionInDays, + }); + + const role = new cdk.aws_iam.Role(this, 'FlowLogsRole', { + assumedBy: new cdk.aws_iam.ServicePrincipal('vpc-flow-logs.amazonaws.com'), + }); + + role.addToPrincipalPolicy( + new cdk.aws_iam.PolicyStatement({ + actions: [ + 'logs:CreateLogDelivery', + 'logs:CreateLogGroup', + 'logs:CreateLogStream', + 'logs:DeleteLogDelivery', + 'logs:DescribeLogGroups', + 'logs:DescribeLogStreams', + 'logs:PutLogEvents', + ], + resources: [logGroup.logGroupArn], + }), + ); + + new cdk.aws_ec2.CfnFlowLog(this, 'CloudWatchFlowLog', { + deliverLogsPermissionArn: role.roleArn, // import * as logs from 'aws-cdk-lib/aws-logs'; + logDestinationType: 'cloud-watch-logs', + logDestination: logGroup.logGroupArn, + resourceId: this.vpcId, + resourceType: 'VPC', + trafficType: options.trafficType, + maxAggregationInterval, + logFormat: options.logFormat, + }); + } + + // Destination: S3 + if (options.destinations.includes('s3')) { + new cdk.aws_ec2.CfnFlowLog(this, 'S3FlowLog', { + logDestinationType: 's3', + logDestination: options.bucketArn, + resourceId: this.vpcId, + resourceType: 'VPC', + trafficType: options.trafficType, + maxAggregationInterval, + logFormat: options.logFormat, + }); + } + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/create-members/index.ts b/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/create-members/index.ts new file mode 100644 index 000000000..9b1333783 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/create-members/index.ts @@ -0,0 +1,114 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { throttlingBackOff } from '@aws-accelerator/utils'; +import * as AWS from 'aws-sdk'; +AWS.config.logger = console; + +/** + * enable-guardduty - lambda handler + * + * @param event + * @returns + */ +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + Status: string | undefined; + StatusCode: number | undefined; + } + | undefined +> { + const region = event.ResourceProperties['region']; + const partition = event.ResourceProperties['partition']; + const enableS3Protection: boolean = event.ResourceProperties['enableS3Protection'] === 'true'; + + let organizationsClient: AWS.Organizations; + if (partition === 'aws-us-gov') { + organizationsClient = new AWS.Organizations({ region: 'us-gov-west-1' }); + } else { + organizationsClient = new AWS.Organizations({ region: 'us-east-1' }); + } + + const guardDutyClient = new AWS.GuardDuty({ region: region }); + + const detectorId = await getDetectorId(guardDutyClient); + + let nextToken: string | undefined = undefined; + + switch (event.RequestType) { + case 'Create': + case 'Update': + console.log('starting - CreateMembersCommand'); + const allAccounts: AWS.GuardDuty.AccountDetail[] = []; + + do { + const page = await throttlingBackOff(() => + organizationsClient.listAccounts({ NextToken: nextToken }).promise(), + ); + for (const account of page.Accounts ?? []) { + allAccounts.push({ AccountId: account.Id!, Email: account.Email! }); + } + nextToken = page.NextToken; + } while (nextToken); + + await throttlingBackOff(() => + guardDutyClient.createMembers({ DetectorId: detectorId!, AccountDetails: allAccounts }).promise(), + ); + + await throttlingBackOff(() => + guardDutyClient + .updateOrganizationConfiguration({ + AutoEnable: true, + DetectorId: detectorId!, + DataSources: { S3Logs: { AutoEnable: enableS3Protection } }, + }) + .promise(), + ); + + return { Status: 'Success', StatusCode: 200 }; + + case 'Delete': + const existingMemberAccountIds: string[] = []; + nextToken = undefined; + do { + const page = await throttlingBackOff(() => + guardDutyClient.listMembers({ DetectorId: detectorId!, NextToken: nextToken }).promise(), + ); + for (const member of page.Members ?? []) { + console.log(member); + existingMemberAccountIds.push(member.AccountId!); + } + nextToken = page.NextToken; + } while (nextToken); + + if (existingMemberAccountIds.length > 0) { + await throttlingBackOff(() => + guardDutyClient + .disassociateMembers({ AccountIds: existingMemberAccountIds, DetectorId: detectorId! }) + .promise(), + ); + + await throttlingBackOff(() => + guardDutyClient.deleteMembers({ AccountIds: existingMemberAccountIds, DetectorId: detectorId! }).promise(), + ); + } + + return { Status: 'Success', StatusCode: 200 }; + } +} + +async function getDetectorId(guardDutyClient: AWS.GuardDuty): Promise { + const response = await throttlingBackOff(() => guardDutyClient.listDetectors({}).promise()); + console.log(response); + return response.DetectorIds!.length === 1 ? response.DetectorIds![0] : undefined; +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/create-members/package.json b/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/create-members/package.json new file mode 100644 index 000000000..c4a5a4487 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/create-members/package.json @@ -0,0 +1,45 @@ +{ + "name": "@aws-accelerator/constructs-aws-guardduty-create-members", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --outfile=./dist/index.js --platform=node --target=node14 --external:aws-sdk index.ts", + "test": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "esbuild": "0.14.2", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2" + }, + "peerDependencies": { + "eslint": "8.4.1" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1001.0" + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/create-members/tsconfig.json b/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/create-members/tsconfig.json new file mode 100644 index 000000000..272282f1f --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/create-members/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} \ No newline at end of file diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/create-publishing-destination/index.ts b/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/create-publishing-destination/index.ts new file mode 100644 index 000000000..e417d8f8c --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/create-publishing-destination/index.ts @@ -0,0 +1,111 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { throttlingBackOff } from '@aws-accelerator/utils'; +import * as AWS from 'aws-sdk'; +AWS.config.logger = console; + +/** + * enable-guardduty - lambda handler + * + * @param event + * @returns + */ +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + Status: string | undefined; + StatusCode: number | undefined; + } + | undefined +> { + const region = event.ResourceProperties['region']; + const exportDestinationType = event.ResourceProperties['exportDestinationType']; + const bucketArn = event.ResourceProperties['bucketArn']; + const kmsKeyArn = event.ResourceProperties['kmsKeyArn']; + + const guardDutyClient = new AWS.GuardDuty({ region: region }); + + let detectorId = await getDetectorId(guardDutyClient); + + switch (event.RequestType) { + case 'Create': + case 'Update': + console.log('starting - CreatePublishingDestination'); + if (!detectorId) { + await throttlingBackOff(() => + guardDutyClient + .createDetector({ + Enable: true, + }) + .promise(), + ); + + detectorId = await getDetectorId(guardDutyClient); + } + + const listPublishingDestinationResponse = await throttlingBackOff(() => + guardDutyClient + .listPublishingDestinations({ + DetectorId: detectorId!, + }) + .promise(), + ); + + if (listPublishingDestinationResponse.Destinations!.length === 0) { + console.log('starting CreatePublishingDestinationCommand'); + + await throttlingBackOff(() => + guardDutyClient + .createPublishingDestination({ + DetectorId: detectorId!, + DestinationType: exportDestinationType, + DestinationProperties: { DestinationArn: bucketArn, KmsKeyArn: kmsKeyArn }, + }) + .promise(), + ); + } + + return { Status: 'Success', StatusCode: 200 }; + + case 'Delete': + const response = await throttlingBackOff(() => + guardDutyClient + .listPublishingDestinations({ + DetectorId: detectorId!, + }) + .promise(), + ); + + const destinationId = + response.Destinations ?? [].length === 1 ? response.Destinations![0].DestinationId : undefined; + + if (response.Destinations!.length === 1) { + await throttlingBackOff(() => + guardDutyClient + .deletePublishingDestination({ + DetectorId: detectorId!, + DestinationId: destinationId!, + }) + .promise(), + ); + } + + return { Status: 'Success', StatusCode: 200 }; + } +} + +async function getDetectorId(guardDutyClient: AWS.GuardDuty): Promise { + const response = await throttlingBackOff(() => guardDutyClient.listDetectors({}).promise()); + console.log(response); + return response.DetectorIds!.length === 1 ? response.DetectorIds![0] : undefined; +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/create-publishing-destination/package.json b/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/create-publishing-destination/package.json new file mode 100644 index 000000000..f98329905 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/create-publishing-destination/package.json @@ -0,0 +1,44 @@ +{ + "name": "@aws-accelerator/constructs-aws-guardduty-create-publishing-destination", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --outfile=./dist/index.js --platform=node --target=node14 --external:aws-sdk index.ts", + "test": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2" + }, + "peerDependencies": { + "eslint": "8.4.1" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1001.0" + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/create-publishing-destination/tsconfig.json b/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/create-publishing-destination/tsconfig.json new file mode 100644 index 000000000..272282f1f --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/create-publishing-destination/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} \ No newline at end of file diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/enable-organization-admin-account/index.ts b/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/enable-organization-admin-account/index.ts new file mode 100644 index 000000000..a0663ec2d --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/enable-organization-admin-account/index.ts @@ -0,0 +1,110 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { throttlingBackOff } from '@aws-accelerator/utils'; +import * as AWS from 'aws-sdk'; +AWS.config.logger = console; + +/** + * enable-guardduty - lambda handler + * + * @param event + * @returns + */ +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + Status: string | undefined; + StatusCode: number | undefined; + } + | undefined +> { + const region = event.ResourceProperties['region']; + const adminAccountId = event.ResourceProperties['adminAccountId']; + + const guardDutyClient = new AWS.GuardDuty({ region: region }); + + const guardDutyAdminAccount = await isGuardDutyEnable(guardDutyClient, adminAccountId); + + switch (event.RequestType) { + case 'Create': + case 'Update': + if (guardDutyAdminAccount.status) { + if (guardDutyAdminAccount.accountId === adminAccountId) { + console.warn( + `GuardDuty admin account ${guardDutyAdminAccount.accountId} is already an admin account as status is ${guardDutyAdminAccount.status}, in ${region} region. No action needed`, + ); + return { Status: 'Success', StatusCode: 200 }; + } else { + console.warn( + `GuardDuty delegated admin is already set to ${guardDutyAdminAccount.accountId} account can not assign another delegated account`, + ); + } + } else { + console.log( + `Started enableOrganizationAdminAccount function in ${event.ResourceProperties['region']} region for account ${adminAccountId}`, + ); + await throttlingBackOff(() => + guardDutyClient.enableOrganizationAdminAccount({ AdminAccountId: adminAccountId }).promise(), + ); + } + + return { Status: 'Success', StatusCode: 200 }; + + case 'Delete': + if (guardDutyAdminAccount.accountId) { + if (guardDutyAdminAccount.accountId === adminAccountId) { + console.log( + `Started disableOrganizationAdminAccount function in ${event.ResourceProperties['region']} region for account ${adminAccountId}`, + ); + await throttlingBackOff(() => + guardDutyClient + .disableOrganizationAdminAccount({ + AdminAccountId: adminAccountId, + }) + .promise(), + ); + } + } + + return { Status: 'Success', StatusCode: 200 }; + } +} + +async function isGuardDutyEnable( + guardDutyClient: AWS.GuardDuty, + adminAccountId: string, +): Promise<{ accountId: string | undefined; status: string | undefined }> { + const adminAccounts: AWS.GuardDuty.AdminAccount[] = []; + let nextToken: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => + guardDutyClient.listOrganizationAdminAccounts({ NextToken: nextToken }).promise(), + ); + for (const account of page.AdminAccounts ?? []) { + adminAccounts.push(account); + } + nextToken = page.NextToken; + } while (nextToken); + if (adminAccounts.length === 0) { + return { accountId: undefined, status: undefined }; + } + if (adminAccounts.length > 1) { + throw new Error('Multiple admin accounts for GuardDuty in organization'); + } + + if (adminAccounts[0].AdminAccountId === adminAccountId && adminAccounts[0].AdminStatus === 'DISABLE_IN_PROGRESS') { + throw new Error(`Admin account ${adminAccounts[0].AdminAccountId} is in ${adminAccounts[0].AdminStatus}`); + } + + return { accountId: adminAccounts[0].AdminAccountId, status: adminAccounts[0].AdminStatus }; +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/enable-organization-admin-account/package.json b/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/enable-organization-admin-account/package.json new file mode 100644 index 000000000..953bfb6d5 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/enable-organization-admin-account/package.json @@ -0,0 +1,45 @@ +{ + "name": "@aws-accelerator/constructs-aws-guardduty-enable-organization-admin-account", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --outfile=./dist/index.js --platform=node --target=node14 --external:aws-sdk index.ts", + "test": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "esbuild": "0.14.2", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2" + }, + "peerDependencies": { + "eslint": "8.4.1" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1001.0" + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/enable-organization-admin-account/tsconfig.json b/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/enable-organization-admin-account/tsconfig.json new file mode 100644 index 000000000..272282f1f --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/enable-organization-admin-account/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} \ No newline at end of file diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/guardduty-detector-config.ts b/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/guardduty-detector-config.ts new file mode 100644 index 000000000..f5e62bbd1 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/guardduty-detector-config.ts @@ -0,0 +1,110 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +const path = require('path'); + +/** + * Export config destination types + */ +export enum GuardDutyExportConfigDestinationTypes { + S3 = 's3', +} + +/** + * Initialized GuardDutyDetectorConfigProps properties + */ +export interface GuardDutyDetectorConfigProps { + /** + * Export config enable flag + */ + readonly isExportConfigEnable: boolean; + /** + * Export config destination type, example s3 + */ + readonly exportDestination: string; + /** + * FindingPublishingFrequency + */ + readonly exportFrequency: string; + /** + * Custom resource lambda log group encryption key + */ + readonly kmsKey: cdk.aws_kms.Key; + /** + * Custom resource lambda log retention in days + */ + readonly logRetentionInDays: number; +} + +/** + /** + * Class to GuardDuty Detector Members + */ +export class GuardDutyDetectorConfig extends Construct { + public readonly id: string; + + constructor(scope: Construct, id: string, props: GuardDutyDetectorConfigProps) { + super(scope, id); + + const RESOURCE_TYPE = 'Custom::GuardDutyUpdateDetector'; + + const provider = cdk.CustomResourceProvider.getOrCreateProvider(this, RESOURCE_TYPE, { + codeDirectory: path.join(__dirname, 'update-detector-config/dist'), + runtime: cdk.CustomResourceProviderRuntime.NODEJS_14_X, + policyStatements: [ + { + Sid: 'GuardDutyUpdateDetectorTaskGuardDutyActions', + Effect: 'Allow', + Action: [ + 'guardduty:ListDetectors', + 'guardduty:ListMembers', + 'guardduty:UpdateDetector', + 'guardduty:UpdateMemberDetectors', + ], + Resource: '*', + }, + ], + }); + + const resource = new cdk.CustomResource(this, 'Resource', { + resourceType: RESOURCE_TYPE, + serviceToken: provider.serviceToken, + properties: { + region: cdk.Stack.of(this).region, + isExportConfigEnable: props.isExportConfigEnable, + exportDestination: props.exportDestination, + exportFrequency: props.exportFrequency, + }, + }); + + /** + * Singleton pattern to define the log group for the singleton function + * in the stack + */ + const stack = cdk.Stack.of(scope); + const logGroup = + (stack.node.tryFindChild(`${provider.node.id}LogGroup`) as cdk.aws_logs.LogGroup) ?? + new cdk.aws_logs.LogGroup(stack, `${provider.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${(provider.node.findChild('Handler') as cdk.aws_lambda.CfnFunction).ref}`, + retention: props.logRetentionInDays, + encryptionKey: props.kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + resource.node.addDependency(logGroup); + + this.id = resource.ref; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/guardduty-members.ts b/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/guardduty-members.ts new file mode 100644 index 000000000..1ae06f884 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/guardduty-members.ts @@ -0,0 +1,115 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +const path = require('path'); + +/** + * Initialized GuardDutyMembersProps properties + */ +export interface GuardDutyMembersProps { + /** + * S3 Protection enable flag + */ + readonly enableS3Protection: boolean; + /** + * Custom resource lambda log group encryption key + */ + readonly kmsKey: cdk.aws_kms.Key; + /** + * Custom resource lambda log retention in days + */ + readonly logRetentionInDays: number; +} + +/** + /** + * Class to GuardDuty Members + */ +export class GuardDutyMembers extends Construct { + public readonly id: string; + + constructor(scope: Construct, id: string, props: GuardDutyMembersProps) { + super(scope, id); + + const RESOURCE_TYPE = 'Custom::GuardDutyCreateMembers'; + + const provider = cdk.CustomResourceProvider.getOrCreateProvider(this, RESOURCE_TYPE, { + codeDirectory: path.join(__dirname, 'create-members/dist'), + runtime: cdk.CustomResourceProviderRuntime.NODEJS_14_X, + policyStatements: [ + { + Sid: 'GuardDutyCreateMembersTaskOrganizationAction', + Effect: 'Allow', + Action: ['organizations:ListAccounts'], + Resource: '*', + Condition: { + StringLikeIfExists: { + 'organizations:ListAccounts': ['guardduty.amazonaws.com'], + }, + }, + }, + { + Sid: 'GuardDutyCreateMembersTaskGuardDutyActions', + Effect: 'Allow', + Action: [ + 'guardDuty:ListDetectors', + 'guardDuty:ListOrganizationAdminAccounts', + 'guardDuty:UpdateOrganizationConfiguration', + 'guardduty:CreateMembers', + 'guardduty:DeleteMembers', + 'guardduty:DisassociateMembers', + 'guardduty:ListDetectors', + 'guardduty:ListMembers', + ], + Resource: '*', + }, + { + Sid: 'ServiceLinkedRoleSecurityHub', + Effect: 'Allow', + Action: ['iam:CreateServiceLinkedRole'], + Resource: ['*'], + }, + ], + }); + + const resource = new cdk.CustomResource(this, 'Resource', { + resourceType: RESOURCE_TYPE, + serviceToken: provider.serviceToken, + properties: { + region: cdk.Stack.of(this).region, + partition: cdk.Aws.PARTITION, + enableS3Protection: props.enableS3Protection, + }, + }); + + /** + * Singleton pattern to define the log group for the singleton function + * in the stack + */ + const stack = cdk.Stack.of(scope); + const logGroup = + (stack.node.tryFindChild(`${provider.node.id}LogGroup`) as cdk.aws_logs.LogGroup) ?? + new cdk.aws_logs.LogGroup(stack, `${provider.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${(provider.node.findChild('Handler') as cdk.aws_lambda.CfnFunction).ref}`, + retention: props.logRetentionInDays, + encryptionKey: props.kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + resource.node.addDependency(logGroup); + + this.id = resource.ref; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/guardduty-organization-admin-account.ts b/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/guardduty-organization-admin-account.ts new file mode 100644 index 000000000..f5ca102aa --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/guardduty-organization-admin-account.ts @@ -0,0 +1,120 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +const path = require('path'); + +/** + * Initialized GuardDutyOrganizationalAdminAccountProps properties + */ +export interface GuardDutyOrganizationalAdminAccountProps { + /** + * Admin account id + */ + readonly adminAccountId: string; + /** + * Custom resource lambda log group encryption key + */ + readonly kmsKey: cdk.aws_kms.Key; + /** + * Custom resource lambda log retention in days + */ + readonly logRetentionInDays: number; +} + +/** + * Class for GuardDutyOrganizationAdminAccount + */ +export class GuardDutyOrganizationAdminAccount extends Construct { + public readonly id: string; + + constructor(scope: Construct, id: string, props: GuardDutyOrganizationalAdminAccountProps) { + super(scope, id); + + const RESOURCE_TYPE = 'Custom::GuardDutyEnableOrganizationAdminAccount'; + + const provider = cdk.CustomResourceProvider.getOrCreateProvider(this, RESOURCE_TYPE, { + codeDirectory: path.join(__dirname, 'enable-organization-admin-account/dist'), + runtime: cdk.CustomResourceProviderRuntime.NODEJS_14_X, + policyStatements: [ + { + Sid: 'GuardDutyEnableOrganizationAdminAccountTaskOrganizationActions', + Effect: 'Allow', + Action: [ + 'organizations:DeregisterDelegatedAdministrator', + 'organizations:DescribeOrganization', + 'organizations:EnableAWSServiceAccess', + 'organizations:ListAWSServiceAccessForOrganization', + 'organizations:ListAccounts', + 'organizations:ListDelegatedAdministrators', + 'organizations:RegisterDelegatedAdministrator', + 'organizations:ServicePrincipal', + 'organizations:UpdateOrganizationConfiguration', + ], + Resource: '*', + Condition: { + StringLikeIfExists: { + 'organizations:DeregisterDelegatedAdministrator': ['guardduty.amazonaws.com'], + 'organizations:DescribeOrganization': ['guardduty.amazonaws.com'], + 'organizations:EnableAWSServiceAccess': ['guardduty.amazonaws.com'], + 'organizations:ListAWSServiceAccessForOrganization': ['guardduty.amazonaws.com'], + 'organizations:ListAccounts': ['guardduty.amazonaws.com'], + 'organizations:ListDelegatedAdministrators': ['guardduty.amazonaws.com'], + 'organizations:RegisterDelegatedAdministrator': ['guardduty.amazonaws.com'], + 'organizations:ServicePrincipal': ['guardduty.amazonaws.com'], + 'organizations:UpdateOrganizationConfiguration': ['guardduty.amazonaws.com'], + }, + }, + }, + { + Sid: 'GuardDutyEnableOrganizationAdminAccountTaskGuardDutyActions', + Effect: 'Allow', + Action: [ + 'GuardDuty:EnableOrganizationAdminAccount', + 'GuardDuty:ListOrganizationAdminAccounts', + 'guardduty:DisableOrganizationAdminAccount', + ], + Resource: '*', + }, + ], + }); + + const resource = new cdk.CustomResource(this, 'Resource', { + resourceType: RESOURCE_TYPE, + serviceToken: provider.serviceToken, + properties: { + region: cdk.Stack.of(this).region, + adminAccountId: props.adminAccountId, + }, + }); + + /** + * Singleton pattern to define the log group for the singleton function + * in the stack + */ + const stack = cdk.Stack.of(scope); + const logGroup = + (stack.node.tryFindChild(`${provider.node.id}LogGroup`) as cdk.aws_logs.LogGroup) ?? + new cdk.aws_logs.LogGroup(stack, `${provider.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${(provider.node.findChild('Handler') as cdk.aws_lambda.CfnFunction).ref}`, + retention: props.logRetentionInDays, + encryptionKey: props.kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + resource.node.addDependency(logGroup); + + this.id = resource.ref; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/guardduty-publishing-destination.ts b/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/guardduty-publishing-destination.ts new file mode 100644 index 000000000..79c909699 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/guardduty-publishing-destination.ts @@ -0,0 +1,100 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +const path = require('path'); + +/** + * Initialized GuardDutyPublishingDestinationProps properties + */ +export interface GuardDutyPublishingDestinationProps { + /** + * Export destination type + */ + readonly exportDestinationType: string; + /** + * Publishing destination bucket arn + */ + readonly bucketArn: string; + /** + * Custom resource lambda log group encryption key + */ + readonly kmsKey: cdk.aws_kms.Key; + /** + * Custom resource lambda log retention in days + */ + readonly logRetentionInDays: number; +} + +/** + * Class - GuardDutyPublishingDestination + */ +export class GuardDutyPublishingDestination extends Construct { + public readonly id: string = ''; + + constructor(scope: Construct, id: string, props: GuardDutyPublishingDestinationProps) { + super(scope, id); + + const RESOURCE_TYPE = 'Custom::GuardDutyCreatePublishingDestinationCommand'; + + const provider = cdk.CustomResourceProvider.getOrCreateProvider(this, RESOURCE_TYPE, { + codeDirectory: path.join(__dirname, 'create-publishing-destination/dist'), + runtime: cdk.CustomResourceProviderRuntime.NODEJS_14_X, + policyStatements: [ + { + Sid: 'GuardDutyCreatePublishingDestinationCommandTaskGuardDutyActions', + Effect: 'Allow', + Action: [ + 'guardDuty:CreateDetector', + 'guardDuty:CreatePublishingDestination', + 'guardDuty:DeletePublishingDestination', + 'guardDuty:ListDetectors', + 'guardDuty:ListPublishingDestinations', + 'iam:CreateServiceLinkedRole', + ], + Resource: '*', + }, + ], + }); + + const resource = new cdk.CustomResource(this, 'Resource', { + resourceType: RESOURCE_TYPE, + serviceToken: provider.serviceToken, + properties: { + region: cdk.Stack.of(this).region, + exportDestinationType: props.exportDestinationType, + bucketArn: props.bucketArn, + kmsKeyArn: props.kmsKey.keyArn, + }, + }); + + /** + * Singleton pattern to define the log group for the singleton function + * in the stack + */ + const stack = cdk.Stack.of(scope); + const logGroup = + (stack.node.tryFindChild(`${provider.node.id}LogGroup`) as cdk.aws_logs.LogGroup) ?? + new cdk.aws_logs.LogGroup(stack, `${provider.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${(provider.node.findChild('Handler') as cdk.aws_lambda.CfnFunction).ref}`, + retention: props.logRetentionInDays, + encryptionKey: props.kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + resource.node.addDependency(logGroup); + + this.id = resource.ref; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/update-detector-config/index.ts b/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/update-detector-config/index.ts new file mode 100644 index 000000000..d234ac04f --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/update-detector-config/index.ts @@ -0,0 +1,118 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { throttlingBackOff } from '@aws-accelerator/utils'; +import * as AWS from 'aws-sdk'; +AWS.config.logger = console; + +/** + * GuardDutyUpdateDetector - lambda handler + * + * @param event + * @returns + */ +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + Status: string | undefined; + StatusCode: number | undefined; + } + | undefined +> { + const region = event.ResourceProperties['region']; + const adminAccountId = event.ResourceProperties['adminAccountId']; + const isExportConfigEnable = event.ResourceProperties['isExportConfigEnable'] === 'true'; + const exportDestination = event.ResourceProperties['exportDestination']; + const exportFrequency = event.ResourceProperties['exportFrequency']; + + const guardDutyClient = new AWS.GuardDuty({ region: region }); + const detectorId = await getDetectorId(guardDutyClient); + + const existingMemberAccountIds: string[] = [adminAccountId]; + + let nextToken: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => + guardDutyClient.listMembers({ DetectorId: detectorId, NextToken: nextToken }).promise(), + ); + for (const member of page.Members ?? []) { + console.log(member); + existingMemberAccountIds.push(member.AccountId!); + } + nextToken = page.NextToken; + } while (nextToken); + + switch (event.RequestType) { + case 'Create': + case 'Update': + console.log('starting - CreateMembersCommand'); + if (isExportConfigEnable && exportDestination === 's3') { + await throttlingBackOff(() => + guardDutyClient + .updateMemberDetectors({ + DetectorId: detectorId, + AccountIds: existingMemberAccountIds, + DataSources: { S3Logs: { Enable: isExportConfigEnable } }, + }) + .promise(), + ); + + await throttlingBackOff(() => + guardDutyClient + .updateDetector({ + DetectorId: detectorId, + Enable: true, + FindingPublishingFrequency: exportFrequency, + DataSources: { S3Logs: { Enable: isExportConfigEnable } }, + }) + .promise(), + ); + } + + return { Status: 'Success', StatusCode: 200 }; + + case 'Delete': + if (isExportConfigEnable && exportDestination === 's3') { + await throttlingBackOff(() => + guardDutyClient + .updateDetector({ + DetectorId: detectorId, + Enable: false, + FindingPublishingFrequency: exportFrequency, + DataSources: { S3Logs: { Enable: false } }, + }) + .promise(), + ); + + await throttlingBackOff(() => + guardDutyClient + .updateMemberDetectors({ + DetectorId: detectorId, + AccountIds: existingMemberAccountIds, + DataSources: { S3Logs: { Enable: false } }, + }) + .promise(), + ); + } + + return { Status: 'Success', StatusCode: 200 }; + } +} + +async function getDetectorId(guardDutyClient: AWS.GuardDuty): Promise { + const response = await throttlingBackOff(() => guardDutyClient.listDetectors({}).promise()); + console.log(response); + if (response.DetectorIds) { + return response.DetectorIds[0]; + } + throw new Error(`GuardDuty not enabled`); +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/update-detector-config/package.json b/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/update-detector-config/package.json new file mode 100644 index 000000000..53c4148bf --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/update-detector-config/package.json @@ -0,0 +1,45 @@ +{ + "name": "@aws-accelerator/constructs-aws-guardduty-update-detector-config", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --outfile=./dist/index.js --platform=node --target=node14 --external:aws-sdk index.ts", + "test": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "esbuild": "0.14.2", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2" + }, + "peerDependencies": { + "eslint": "8.4.1" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1001.0" + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/update-detector-config/tsconfig.json b/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/update-detector-config/tsconfig.json new file mode 100644 index 000000000..272282f1f --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-guardduty/update-detector-config/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} \ No newline at end of file diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-iam/password-policy.ts b/source/packages/@aws-accelerator/constructs/lib/aws-iam/password-policy.ts new file mode 100644 index 000000000..158427a99 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-iam/password-policy.ts @@ -0,0 +1,98 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +const path = require('path'); + +/** + * PasswordPolicyProps properties + */ +export interface PasswordPolicyProps { + readonly allowUsersToChangePassword: boolean; + readonly hardExpiry: boolean; + readonly requireUppercaseCharacters: boolean; + readonly requireLowercaseCharacters: boolean; + readonly requireSymbols: boolean; + readonly requireNumbers: boolean; + readonly minimumPasswordLength: number; + readonly passwordReusePrevention: number; + readonly maxPasswordAge: number; + /** + * Custom resource lambda log group encryption key + */ + readonly kmsKey: cdk.aws_kms.Key; + /** + * Custom resource lambda log retention in days + */ + readonly logRetentionInDays: number; +} + +/** + * Class to Update Account Password Policy + */ +export class PasswordPolicy extends Construct { + public readonly id: string; + + constructor(scope: Construct, id: string, props: PasswordPolicyProps) { + super(scope, id); + + const RESOURCE_TYPE = 'Custom::IamUpdateAccountPasswordPolicy'; + + const provider = cdk.CustomResourceProvider.getOrCreateProvider(this, RESOURCE_TYPE, { + codeDirectory: path.join(__dirname, 'update-account-password-policy/dist'), + runtime: cdk.CustomResourceProviderRuntime.NODEJS_14_X, + policyStatements: [ + { + Effect: 'Allow', + Action: ['iam:UpdateAccountPasswordPolicy'], + Resource: '*', + }, + ], + }); + + const resource = new cdk.CustomResource(this, 'Resource', { + resourceType: RESOURCE_TYPE, + serviceToken: provider.serviceToken, + properties: { + allowUsersToChangePassword: props.allowUsersToChangePassword, + hardExpiry: props.hardExpiry, + requireUppercaseCharacters: props.requireUppercaseCharacters, + requireLowercaseCharacters: props.requireLowercaseCharacters, + requireSymbols: props.requireSymbols, + requireNumbers: props.requireNumbers, + minimumPasswordLength: props.minimumPasswordLength, + passwordReusePrevention: props.passwordReusePrevention, + maxPasswordAge: props.maxPasswordAge, + }, + }); + + /** + * Singleton pattern to define the log group for the singleton function + * in the stack + */ + const stack = cdk.Stack.of(scope); + const logGroup = + (stack.node.tryFindChild(`${provider.node.id}LogGroup`) as cdk.aws_logs.LogGroup) ?? + new cdk.aws_logs.LogGroup(stack, `${provider.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${(provider.node.findChild('Handler') as cdk.aws_lambda.CfnFunction).ref}`, + retention: props.logRetentionInDays, + encryptionKey: props.kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + resource.node.addDependency(logGroup); + + this.id = resource.ref; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-iam/update-account-password-policy/index.ts b/source/packages/@aws-accelerator/constructs/lib/aws-iam/update-account-password-policy/index.ts new file mode 100644 index 000000000..87a6f5282 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-iam/update-account-password-policy/index.ts @@ -0,0 +1,62 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { throttlingBackOff } from '@aws-accelerator/utils'; +import * as AWS from 'aws-sdk'; +AWS.config.logger = console; + +/** + * update-account-password-policy - lambda handler + * + * @param event + * @returns + */ +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + PhysicalResourceId: string | undefined; + Status: string; + } + | undefined +> { + switch (event.RequestType) { + case 'Create': + case 'Update': + const iamClient = new AWS.IAM({}); + await throttlingBackOff(() => + iamClient + .updateAccountPasswordPolicy({ + AllowUsersToChangePassword: event.ResourceProperties['allowUsersToChangePassword'] === 'true', + HardExpiry: event.ResourceProperties['hardExpiry'] === 'true', + RequireUppercaseCharacters: event.ResourceProperties['requireUppercaseCharacters'] === 'true', + RequireLowercaseCharacters: event.ResourceProperties['requireLowercaseCharacters'] === 'true', + RequireSymbols: event.ResourceProperties['requireSymbols'] === 'true', + RequireNumbers: event.ResourceProperties['requireNumbers'] === 'true', + MinimumPasswordLength: event.ResourceProperties['minimumPasswordLength'], + PasswordReusePrevention: event.ResourceProperties['passwordReusePrevention'], + MaxPasswordAge: event.ResourceProperties['maxPasswordAge'], + }) + .promise(), + ); + return { + PhysicalResourceId: 'update-account-password-policy', + Status: 'SUCCESS', + }; + + case 'Delete': + // Do Nothing + return { + PhysicalResourceId: event.PhysicalResourceId, + Status: 'SUCCESS', + }; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-iam/update-account-password-policy/package.json b/source/packages/@aws-accelerator/constructs/lib/aws-iam/update-account-password-policy/package.json new file mode 100644 index 000000000..d592e916e --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-iam/update-account-password-policy/package.json @@ -0,0 +1,43 @@ +{ + "name": "@aws-accelerator/constructs-aws-iam-update-account-password-policy", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --outfile=./dist/index.js --platform=node --target=node14 --external:aws-sdk index.ts", + "test": "", + "testreport": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "esbuild": "0.14.2", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1001.0" + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-iam/update-account-password-policy/tsconfig.json b/source/packages/@aws-accelerator/constructs/lib/aws-iam/update-account-password-policy/tsconfig.json new file mode 100644 index 000000000..ebcb98a79 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-iam/update-account-password-policy/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-kms/key-lookup.ts b/source/packages/@aws-accelerator/constructs/lib/aws-kms/key-lookup.ts new file mode 100644 index 000000000..fffa9ac16 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-kms/key-lookup.ts @@ -0,0 +1,73 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import { SsmParameterLookup } from '../aws-ssm/ssm-parameter-lookup'; + +/** + * Initialized KeyLookupProps properties + */ +export interface KeyLookupProps { + /** + * SSM parameter name where key arn is stored + */ + readonly keyArnParameterName: string; + /** + * Key account id + */ + readonly accountId: string; + /** + * The name of the cross account role to use when accessing + */ + readonly roleName: string; + /** + * Custom resource lambda log group encryption key + */ + readonly kmsKey?: cdk.aws_kms.Key; + /** + * Custom resource lambda log retention in days + */ + readonly logRetentionInDays?: number; +} + +/** + * Aws Key class + */ +export class KeyLookup extends Construct { + public readonly key: cdk.aws_kms.Key; + + constructor(scope: Construct, id: string, props: KeyLookupProps) { + super(scope, id); + + let keyArn: string | undefined; + if (cdk.Stack.of(this).account === props.accountId) { + keyArn = cdk.aws_ssm.StringParameter.valueForStringParameter(this, props.keyArnParameterName); + } else { + keyArn = new SsmParameterLookup(this, 'Lookup', { + name: props.keyArnParameterName, + accountId: props.accountId, + roleName: props.roleName, + kmsKey: props.kmsKey, + logRetentionInDays: props.logRetentionInDays, + }).value; + } + + // Accelerator Key + this.key = cdk.aws_kms.Key.fromKeyArn(this, 'Resource', keyArn!) as cdk.aws_kms.Key; + } + + public getKey(): cdk.aws_kms.Key { + return this.key; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-macie/create-member/index.ts b/source/packages/@aws-accelerator/constructs/lib/aws-macie/create-member/index.ts new file mode 100644 index 000000000..001abdd21 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-macie/create-member/index.ts @@ -0,0 +1,162 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { throttlingBackOff } from '@aws-accelerator/utils'; +import * as AWS from 'aws-sdk'; +AWS.config.logger = console; + +/** + * add-macie-members - lambda handler + * + * @param event + * @returns + */ +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + Status: string | undefined; + StatusCode: number | undefined; + } + | undefined +> { + const region = event.ResourceProperties['region']; + const partition = event.ResourceProperties['partition']; + const adminAccountId = event.ResourceProperties['adminAccountId']; + + let organizationsClient: AWS.Organizations; + if (partition === 'aws-us-gov') { + organizationsClient = new AWS.Organizations({ region: 'us-gov-west-1' }); + } else { + organizationsClient = new AWS.Organizations({ region: 'us-east-1' }); + } + + const macie2Client = new AWS.Macie2({ region: region }); + const allAccounts: AWS.Organizations.Account[] = []; + const existingMembers: AWS.Macie2.Member[] = []; + let isEnabled = false; + + let nextToken: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => organizationsClient.listAccounts({ NextToken: nextToken }).promise()); + for (const account of page.Accounts ?? []) { + allAccounts.push(account); + } + nextToken = page.NextToken; + } while (nextToken); + + nextToken = undefined; + do { + const page = await throttlingBackOff(() => macie2Client.listMembers({ nextToken }).promise()); + for (const member of page.members ?? []) { + existingMembers.push(member); + } + nextToken = page.nextToken; + } while (nextToken); + + switch (event.RequestType) { + case 'Create': + case 'Update': + if (!(await isMacieEnable(macie2Client))) { + console.log('start enable of macie'); + await throttlingBackOff(() => + macie2Client + .enableMacie({ + status: 'ENABLED', + }) + .promise(), + ); + } + + for (const account of allAccounts.filter(account => account.Id !== adminAccountId) ?? []) { + if (!existingMembers!.find(member => member.accountId !== account.Id)) { + console.log(`OU account - ${account.Id} macie membership status is "not a macie member", adding as a member`); + await throttlingBackOff(() => + macie2Client + .createMember({ + account: { accountId: account.Id!, email: account.Email! }, + }) + .promise(), + ); + } else { + console.warn( + `OU account - ${account.Id} macie membership status is "a macie member", ignoring create member for the account!!`, + ); + } + } + + isEnabled = await isOrganizationAutoEnabled(macie2Client); + + if (!isEnabled) { + await throttlingBackOff(() => macie2Client.updateOrganizationConfiguration({ autoEnable: true }).promise()); + } else { + console.warn('Delegation admin account Auto-Enable is ON, so ignoring'); + } + return { Status: 'Success', StatusCode: 200 }; + case 'Delete': + for (const account of allAccounts.filter(account => account.Id !== adminAccountId) ?? []) { + if (existingMembers!.find(member => member.accountId !== account.Id)) { + console.log( + `OU account - ${account.Id} macie membership status is "a macie member", removing from member list`, + ); + await throttlingBackOff(() => macie2Client.disassociateMember({ id: account.Id! }).promise()); + await throttlingBackOff(() => macie2Client.deleteMember({ id: account.Id! }).promise()); + } else { + console.warn( + `OU account - ${account.Id} macie membership status is "not a macie member", ignoring removing member list!!`, + ); + } + } + + isEnabled = await isOrganizationAutoEnabled(macie2Client); + if (isEnabled) { + await throttlingBackOff(() => macie2Client.updateOrganizationConfiguration({ autoEnable: false }).promise()); + } else { + console.warn('Delegation admin account Auto-Enable is OFF, so ignoring'); + } + return { Status: 'Success', StatusCode: 200 }; + } +} + +/** + * Checking is organization auto enabled for new account + * @param macieClient + */ +async function isOrganizationAutoEnabled(macieClient: AWS.Macie2): Promise { + console.log('calling isOrganizationAutoEnabled'); + const response = await throttlingBackOff(() => macieClient.describeOrganizationConfiguration({}).promise()); + return response.autoEnable ?? false; +} + +/** + * Checking Macie is enable or disabled + * @param macie2Client + */ +async function isMacieEnable(macie2Client: AWS.Macie2): Promise { + try { + const response = await throttlingBackOff(() => macie2Client.getMacieSession({}).promise()); + return response.status === 'ENABLED'; + } catch ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + e: any + ) { + if ( + // SDKv2 Error Structure + e.code === 'ResourceConflictException' || + // SDKv3 Error Structure + e.name === 'ResourceConflictException' + ) { + console.warn(e.name + ': ' + e.message); + return false; + } + throw new Error(`Macie enable issue error message - ${e}`); + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-macie/create-member/package.json b/source/packages/@aws-accelerator/constructs/lib/aws-macie/create-member/package.json new file mode 100644 index 000000000..cdcf17d82 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-macie/create-member/package.json @@ -0,0 +1,45 @@ +{ + "name": "@aws-accelerator/constructs-aws-macie-create-member", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --outfile=./dist/index.js --platform=node --target=node14 --external:aws-sdk index.ts", + "test": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "esbuild": "0.14.2", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2" + }, + "peerDependencies": { + "eslint": "8.4.1" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1001.0" + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-macie/create-member/tsconfig.json b/source/packages/@aws-accelerator/constructs/lib/aws-macie/create-member/tsconfig.json new file mode 100644 index 000000000..272282f1f --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-macie/create-member/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} \ No newline at end of file diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-macie/enable-macie/index.ts b/source/packages/@aws-accelerator/constructs/lib/aws-macie/enable-macie/index.ts new file mode 100644 index 000000000..4f56f091d --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-macie/enable-macie/index.ts @@ -0,0 +1,115 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { throttlingBackOff } from '@aws-accelerator/utils'; +import * as AWS from 'aws-sdk'; +AWS.config.logger = console; + +/** + * add-macie-members - lambda handler + * + * @param event + * @returns + */ +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + Status: string | undefined; + StatusCode: number | undefined; + } + | undefined +> { + const region = event.ResourceProperties['region']; + const findingPublishingFrequency = event.ResourceProperties['findingPublishingFrequency']; + const isSensitiveSh = event.ResourceProperties['isSensitiveSh'] === 'true'; + + const macie2Client = new AWS.Macie2({ region: region }); + + switch (event.RequestType) { + case 'Create': + case 'Update': + let macieStatus = await isMacieEnable(macie2Client); + if (!macieStatus) { + console.log('start enable of macie'); + await throttlingBackOff(() => + macie2Client + .enableMacie({ + findingPublishingFrequency: findingPublishingFrequency, + status: 'ENABLED', + }) + .promise(), + ); + } + console.log('start update of macie'); + await throttlingBackOff(() => + macie2Client + .updateMacieSession({ + findingPublishingFrequency: findingPublishingFrequency, + status: 'ENABLED', + }) + .promise(), + ); + + // macie status do not change immediately causing failure to other processes, so wait till macie enabled + while (!macieStatus) { + console.log(`checking macie status ${macieStatus}`); + macieStatus = await isMacieEnable(macie2Client); + } + + await throttlingBackOff(() => + macie2Client + .putFindingsPublicationConfiguration({ + securityHubConfiguration: { + publishClassificationFindings: isSensitiveSh, + publishPolicyFindings: true, + }, + }) + .promise(), + ); + + return { Status: 'Success', StatusCode: 200 }; + + case 'Delete': + if (await isMacieEnable(macie2Client)) { + await throttlingBackOff(() => + macie2Client + .disableMacie({ + findingPublishingFrequency: findingPublishingFrequency, + status: 'ENABLED', + }) + .promise(), + ); + } + return { Status: 'Success', StatusCode: 200 }; + } +} + +async function isMacieEnable(macie2Client: AWS.Macie2): Promise { + try { + const response = await throttlingBackOff(() => macie2Client.getMacieSession({}).promise()); + return response.status === 'ENABLED'; + } catch ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + e: any + ) { + if ( + // SDKv2 Error Structure + e.code === 'ResourceConflictException' || + // SDKv3 Error Structure + e.name === 'ResourceConflictException' + ) { + console.warn(e.name + ': ' + e.message); + return false; + } + throw new Error(`Macie enable issue error message - ${e}`); + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-macie/enable-macie/package.json b/source/packages/@aws-accelerator/constructs/lib/aws-macie/enable-macie/package.json new file mode 100644 index 000000000..07ea3734f --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-macie/enable-macie/package.json @@ -0,0 +1,45 @@ +{ + "name": "@aws-accelerator/constructs-aws-macie-enable-macie", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --outfile=./dist/index.js --platform=node --target=node14 --external:aws-sdk index.ts", + "test": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "esbuild": "0.14.2", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2" + }, + "peerDependencies": { + "eslint": "8.4.1" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1001.0" + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-macie/enable-macie/tsconfig.json b/source/packages/@aws-accelerator/constructs/lib/aws-macie/enable-macie/tsconfig.json new file mode 100644 index 000000000..272282f1f --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-macie/enable-macie/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} \ No newline at end of file diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-macie/enable-organization-admin-account/index.ts b/source/packages/@aws-accelerator/constructs/lib/aws-macie/enable-organization-admin-account/index.ts new file mode 100644 index 000000000..efa5917be --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-macie/enable-organization-admin-account/index.ts @@ -0,0 +1,154 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { throttlingBackOff } from '@aws-accelerator/utils'; +import * as AWS from 'aws-sdk'; +AWS.config.logger = console; + +/** + * enableOrganizationAdminAccount - lambda handler + * + * @param event + * @returns + */ +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + Status: string | undefined; + StatusCode: number | undefined; + } + | undefined +> { + const adminAccountId = event.ResourceProperties['adminAccountId']; + const region = event.ResourceProperties['region']; + const macie2Client = new AWS.Macie2({ region: region }); + + const macieDelegatedAccount = await getMacieDelegatedAccount(macie2Client, adminAccountId); + + switch (event.RequestType) { + case 'Create': + case 'Update': + // Enable macie in management account required to create delegated admin account + let macieStatus = await isMacieEnable(macie2Client); + if (!macieStatus) { + console.log('start enable of macie'); + await throttlingBackOff(() => + macie2Client + .enableMacie({ + status: 'ENABLED', + }) + .promise(), + ); + } + + // macie status do not change immediately causing failure to other processes, so wait till macie enabled + while (!macieStatus) { + console.log(`checking macie status ${macieStatus}`); + macieStatus = await isMacieEnable(macie2Client); + } + + if (macieDelegatedAccount.status) { + if (macieDelegatedAccount.accountId === adminAccountId) { + console.warn( + `Macie admin account ${macieDelegatedAccount.accountId} is already an admin account as status is ${macieDelegatedAccount.status}, in ${region} region. No action needed`, + ); + return { Status: 'Success', StatusCode: 200 }; + } else { + console.warn( + `Macie delegated admin is already set to ${macieDelegatedAccount.accountId} account can not assign another delegated account`, + ); + } + } else { + console.log( + `Started enableOrganizationAdminAccount function in ${event.ResourceProperties['region']} region for account ${adminAccountId}`, + ); + await throttlingBackOff(() => + macie2Client.enableOrganizationAdminAccount({ adminAccountId: adminAccountId }).promise(), + ); + } + + return { Status: 'Success', StatusCode: 200 }; + + case 'Delete': + if (macieDelegatedAccount.status) { + console.log( + `Started disableOrganizationAdminAccount function in ${event.ResourceProperties['region']} region for account ${adminAccountId}`, + ); + await throttlingBackOff(() => + macie2Client.disableOrganizationAdminAccount({ adminAccountId: macieDelegatedAccount.accountId! }).promise(), + ); + } + + return { Status: 'Success', StatusCode: 200 }; + } +} + +async function getMacieDelegatedAccount( + macie2Client: AWS.Macie2, + adminAccountId: string, +): Promise<{ accountId: string | undefined; status: boolean }> { + const adminAccounts: AWS.Macie2.AdminAccount[] = []; + + let nextToken: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => macie2Client.listOrganizationAdminAccounts({ nextToken }).promise()); + for (const account of page.adminAccounts ?? []) { + adminAccounts.push(account); + } + nextToken = page.nextToken; + } while (nextToken); + + if (adminAccounts.length === 0) { + return { accountId: undefined, status: false }; + } + if (adminAccounts.length > 1) { + throw new Error('Multiple admin accounts for Macie in organization'); + } + + if (adminAccounts[0].accountId === adminAccountId && adminAccounts[0].status === 'DISABLING_IN_PROGRESS') { + throw new Error(`Admin account ${adminAccounts[0].accountId} is in ${adminAccounts[0].status}`); + } + + return { accountId: adminAccounts[0].accountId, status: true }; +} + +async function isMacieEnable(macie2Client: AWS.Macie2): Promise { + try { + const response = await throttlingBackOff(() => macie2Client.getMacieSession({}).promise()); + return response.status === 'ENABLED'; + } catch ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + e: any + ) { + if ( + // SDKv2 Error Structure + e.code === 'ResourceConflictException' || + // SDKv3 Error Structure + e.name === 'ResourceConflictException' + ) { + console.warn(e.name + ': ' + e.message); + return false; + } + + // This is required when macie is not enabled(first time executing in any management account) AccessDeniedException exception issues + if ( + // SDKv2 Error Structure + e.code === 'AccessDeniedException' || + // SDKv3 Error Structure + e.name === 'AccessDeniedException' + ) { + console.warn(e.name + ': ' + e.message); + return false; + } + throw new Error(`Macie enable issue error message - ${e}`); + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-macie/enable-organization-admin-account/package.json b/source/packages/@aws-accelerator/constructs/lib/aws-macie/enable-organization-admin-account/package.json new file mode 100644 index 000000000..21c4ce10b --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-macie/enable-organization-admin-account/package.json @@ -0,0 +1,46 @@ +{ + "name": "@aws-accelerator/constructs-aws-macie-enable-organization-admin-account", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --outfile=./dist/index.js --platform=node --target=node14 --external:aws-sdk index.ts", + "test": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "esbuild": "0.14.2", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "ts-node": "10.7.0", + "typescript": "4.5.2" + }, + "peerDependencies": { + "eslint": "8.4.1" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1001.0" + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-macie/enable-organization-admin-account/tsconfig.json b/source/packages/@aws-accelerator/constructs/lib/aws-macie/enable-organization-admin-account/tsconfig.json new file mode 100644 index 000000000..272282f1f --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-macie/enable-organization-admin-account/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} \ No newline at end of file diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-macie/macie-export-config-classification.ts b/source/packages/@aws-accelerator/constructs/lib/aws-macie/macie-export-config-classification.ts new file mode 100644 index 000000000..fd16db912 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-macie/macie-export-config-classification.ts @@ -0,0 +1,98 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +const path = require('path'); + +/** + * Initialized MacieExportConfigClassificationProps properties + */ +export interface MacieExportConfigClassificationProps { + /** + * Macie ExportConfigClassification repository bucket name + */ + readonly bucketName: string; + /** + * Bucket key prefix + */ + readonly keyPrefix: string; + /** + * Custom resource lambda log group encryption key + */ + readonly kmsKey: cdk.aws_kms.Key; + /** + * Custom resource lambda log retention in days + */ + readonly logRetentionInDays: number; +} + +/** + * Aws MacieSession export configuration classification + */ +export class MacieExportConfigClassification extends Construct { + public readonly id: string = ''; + + constructor(scope: Construct, id: string, props: MacieExportConfigClassificationProps) { + super(scope, id); + + const RESOURCE_TYPE = 'Custom::MaciePutClassificationExportConfiguration'; + + const provider = cdk.CustomResourceProvider.getOrCreateProvider(this, RESOURCE_TYPE, { + codeDirectory: path.join(__dirname, 'put-export-config-classification/dist'), + runtime: cdk.CustomResourceProviderRuntime.NODEJS_14_X, + policyStatements: [ + { + Sid: 'MaciePutClassificationExportConfigurationTaskMacieActions', + Effect: 'Allow', + Action: [ + 'macie2:EnableMacie', + 'macie2:GetClassificationExportConfiguration', + 'macie2:GetMacieSession', + 'macie2:PutClassificationExportConfiguration', + ], + Resource: '*', + }, + ], + }); + + const resource = new cdk.CustomResource(this, 'Resource', { + resourceType: RESOURCE_TYPE, + serviceToken: provider.serviceToken, + properties: { + region: cdk.Stack.of(this).region, + bucketName: props.bucketName, + keyPrefix: props.keyPrefix, + kmsKeyArn: props.kmsKey.keyArn, + }, + }); + + /** + * Singleton pattern to define the log group for the singleton function + * in the stack + */ + const stack = cdk.Stack.of(scope); + const logGroup = + (stack.node.tryFindChild(`${provider.node.id}LogGroup`) as cdk.aws_logs.LogGroup) ?? + new cdk.aws_logs.LogGroup(stack, `${provider.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${(provider.node.findChild('Handler') as cdk.aws_lambda.CfnFunction).ref}`, + retention: props.logRetentionInDays, + encryptionKey: props.kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + resource.node.addDependency(logGroup); + + this.id = resource.ref; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-macie/macie-members.ts b/source/packages/@aws-accelerator/constructs/lib/aws-macie/macie-members.ts new file mode 100644 index 000000000..e26a17434 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-macie/macie-members.ts @@ -0,0 +1,109 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +const path = require('path'); + +/** + * Initialized AwsMacieMembersProps properties + */ +export interface AwsMacieMembersProps { + /** + * Macie admin account id + */ + readonly adminAccountId: string; + /** + * Custom resource lambda log group encryption key + */ + readonly kmsKey: cdk.aws_kms.Key; + /** + * Custom resource lambda log retention in days + */ + readonly logRetentionInDays: number; +} + +/** + /** + * Class to Aws MacieSession Members + */ +export class MacieMembers extends Construct { + public readonly id: string; + + constructor(scope: Construct, id: string, props: AwsMacieMembersProps) { + super(scope, id); + + const MACIE_RESOURCE_TYPE = 'Custom::MacieCreateMember'; + + const provider = cdk.CustomResourceProvider.getOrCreateProvider(this, MACIE_RESOURCE_TYPE, { + codeDirectory: path.join(__dirname, 'create-member/dist'), + runtime: cdk.CustomResourceProviderRuntime.NODEJS_14_X, + policyStatements: [ + { + Sid: 'MacieCreateMemberTaskOrganizationAction', + Effect: 'Allow', + Action: ['organizations:ListAccounts'], + Resource: '*', + Condition: { + StringLikeIfExists: { + 'organizations:ListAccounts': ['macie.amazonaws.com'], + }, + }, + }, + { + Sid: 'MacieCreateMemberTaskMacieActions', + Effect: 'Allow', + Action: [ + 'macie2:CreateMember', + 'macie2:DeleteMember', + 'macie2:DescribeOrganizationConfiguration', + 'macie2:DisassociateMember', + 'macie2:EnableMacie', + 'macie2:GetMacieSession', + 'macie2:ListMembers', + 'macie2:UpdateOrganizationConfiguration', + ], + Resource: '*', + }, + ], + }); + + const resource = new cdk.CustomResource(this, 'Resource', { + resourceType: MACIE_RESOURCE_TYPE, + serviceToken: provider.serviceToken, + properties: { + region: cdk.Stack.of(this).region, + partition: cdk.Stack.of(this).partition, + adminAccountId: props.adminAccountId, + }, + }); + + /** + * Singleton pattern to define the log group for the singleton function + * in the stack + */ + const stack = cdk.Stack.of(scope); + const logGroup = + (stack.node.tryFindChild(`${provider.node.id}LogGroup`) as cdk.aws_logs.LogGroup) ?? + new cdk.aws_logs.LogGroup(stack, `${provider.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${(provider.node.findChild('Handler') as cdk.aws_lambda.CfnFunction).ref}`, + retention: props.logRetentionInDays, + encryptionKey: props.kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + resource.node.addDependency(logGroup); + + this.id = resource.ref; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-macie/macie-organization-admin-account.ts b/source/packages/@aws-accelerator/constructs/lib/aws-macie/macie-organization-admin-account.ts new file mode 100644 index 000000000..121c2f8e7 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-macie/macie-organization-admin-account.ts @@ -0,0 +1,136 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +const path = require('path'); + +/** + * Initialized MacieOrganizationAdminAccount properties + */ +export interface MacieOrganizationalAdminAccountProps { + /** + * Macie admin account id + */ + readonly adminAccountId: string; + /** + * Custom resource lambda log group encryption key + */ + readonly kmsKey: cdk.aws_kms.Key; + /** + * Custom resource lambda log retention in days + */ + readonly logRetentionInDays: number; +} + +/** + * Aws MacieSession organizational Admin Account + */ +export class MacieOrganizationAdminAccount extends Construct { + public readonly id: string; + + constructor(scope: Construct, id: string, props: MacieOrganizationalAdminAccountProps) { + super(scope, id); + + const MACIE_RESOURCE_TYPE = 'Custom::MacieEnableOrganizationAdminAccount'; + + const provider = cdk.CustomResourceProvider.getOrCreateProvider(this, MACIE_RESOURCE_TYPE, { + codeDirectory: path.join(__dirname, 'enable-organization-admin-account/dist'), + runtime: cdk.CustomResourceProviderRuntime.NODEJS_14_X, + policyStatements: [ + { + Sid: 'MacieEnableOrganizationAdminAccountTaskOrganizationActions', + Effect: 'Allow', + Action: [ + 'organizations:DeregisterDelegatedAdministrator', + 'organizations:DescribeOrganization', + 'organizations:EnableAWSServiceAccess', + 'organizations:ListAWSServiceAccessForOrganization', + 'organizations:ListAccounts', + 'organizations:ListDelegatedAdministrators', + 'organizations:RegisterDelegatedAdministrator', + 'organizations:ServicePrincipal', + 'organizations:UpdateOrganizationConfiguration', + ], + Resource: '*', + Condition: { + StringLikeIfExists: { + 'organizations:DeregisterDelegatedAdministrator': ['macie.amazonaws.com'], + 'organizations:DescribeOrganization': ['macie.amazonaws.com'], + 'organizations:EnableAWSServiceAccess': ['macie.amazonaws.com'], + 'organizations:ListAWSServiceAccessForOrganization': ['macie.amazonaws.com'], + 'organizations:ListAccounts': ['macie.amazonaws.com'], + 'organizations:ListDelegatedAdministrators': ['macie.amazonaws.com'], + 'organizations:RegisterDelegatedAdministrator': ['macie.amazonaws.com'], + 'organizations:ServicePrincipal': ['macie.amazonaws.com'], + 'organizations:UpdateOrganizationConfiguration': ['macie.amazonaws.com'], + }, + }, + }, + { + Sid: 'MacieEnableOrganizationAdminAccountTaskMacieActions', + Effect: 'Allow', + Action: [ + 'macie2:DisableOrganizationAdminAccount', + 'macie2:EnableMacie', + 'macie2:EnableOrganizationAdminAccount', + 'macie2:GetMacieSession', + 'macie2:ListOrganizationAdminAccounts', + 'macie2:DisableOrganizationAdminAccount', + 'macie2:GetMacieSession', + 'macie2:EnableMacie', + ], + Resource: '*', + }, + { + Sid: 'MacieEnableMacieTaskIamAction', + Effect: 'Allow', + Action: ['iam:CreateServiceLinkedRole'], + Resource: '*', + Condition: { + StringLikeIfExists: { + 'iam:CreateServiceLinkedRole': ['macie.amazonaws.com'], + }, + }, + }, + ], + }); + + const resource = new cdk.CustomResource(this, 'Resource', { + resourceType: MACIE_RESOURCE_TYPE, + serviceToken: provider.serviceToken, + properties: { + region: cdk.Stack.of(this).region, + adminAccountId: props.adminAccountId, + }, + }); + + /** + * Singleton pattern to define the log group for the singleton function + * in the stack + */ + const stack = cdk.Stack.of(scope); + const logGroup = + (stack.node.tryFindChild(`${provider.node.id}LogGroup`) as cdk.aws_logs.LogGroup) ?? + new cdk.aws_logs.LogGroup(stack, `${provider.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${(provider.node.findChild('Handler') as cdk.aws_lambda.CfnFunction).ref}`, + retention: props.logRetentionInDays, + encryptionKey: props.kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + resource.node.addDependency(logGroup); + + this.id = resource.ref; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-macie/macie-session.ts b/source/packages/@aws-accelerator/constructs/lib/aws-macie/macie-session.ts new file mode 100644 index 000000000..0fbc7759a --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-macie/macie-session.ts @@ -0,0 +1,109 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +const path = require('path'); + +/** + * Initialized MacieSessionProps properties + */ +export interface MacieSessionProps { + /** + * Findings publishing frequency + */ + readonly findingPublishingFrequency: string; + /** + * Publish sensitive data findings + */ + readonly isSensitiveSh: boolean; + /** + * Custom resource lambda log group encryption key + */ + readonly kmsKey: cdk.aws_kms.Key; + /** + * Custom resource lambda log retention in days + */ + readonly logRetentionInDays: number; +} + +/** + * Aws MacieSession class + */ +export class MacieSession extends Construct { + public readonly id: string = ''; + + constructor(scope: Construct, id: string, props: MacieSessionProps) { + super(scope, id); + + const MACIE_RESOURCE_TYPE = 'Custom::MacieEnableMacie'; + + const provider = cdk.CustomResourceProvider.getOrCreateProvider(this, MACIE_RESOURCE_TYPE, { + codeDirectory: path.join(__dirname, 'enable-macie/dist'), + runtime: cdk.CustomResourceProviderRuntime.NODEJS_14_X, + policyStatements: [ + { + Sid: 'MacieEnableMacieTaskMacieActions', + Effect: 'Allow', + Action: [ + 'macie2:DisableMacie', + 'macie2:EnableMacie', + 'macie2:GetMacieSession', + 'macie2:PutFindingsPublicationConfiguration', + 'macie2:UpdateMacieSession', + ], + Resource: '*', + }, + { + Sid: 'MacieEnableMacieTaskIamAction', + Effect: 'Allow', + Action: ['iam:CreateServiceLinkedRole'], + Resource: '*', + Condition: { + StringLikeIfExists: { + 'iam:CreateServiceLinkedRole': ['macie.amazonaws.com'], + }, + }, + }, + ], + }); + + const resource = new cdk.CustomResource(this, 'Resource', { + resourceType: MACIE_RESOURCE_TYPE, + serviceToken: provider.serviceToken, + properties: { + region: cdk.Stack.of(this).region, + findingPublishingFrequency: props.findingPublishingFrequency, + isSensitiveSh: props.isSensitiveSh, + }, + }); + + /** + * Singleton pattern to define the log group for the singleton function + * in the stack + */ + const stack = cdk.Stack.of(scope); + const logGroup = + (stack.node.tryFindChild(`${provider.node.id}LogGroup`) as cdk.aws_logs.LogGroup) ?? + new cdk.aws_logs.LogGroup(stack, `${provider.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${(provider.node.findChild('Handler') as cdk.aws_lambda.CfnFunction).ref}`, + retention: props.logRetentionInDays, + encryptionKey: props.kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + resource.node.addDependency(logGroup); + + this.id = resource.ref; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-macie/put-export-config-classification/index.ts b/source/packages/@aws-accelerator/constructs/lib/aws-macie/put-export-config-classification/index.ts new file mode 100644 index 000000000..c27f187f5 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-macie/put-export-config-classification/index.ts @@ -0,0 +1,89 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { throttlingBackOff } from '@aws-accelerator/utils'; +import * as AWS from 'aws-sdk'; +AWS.config.logger = console; + +/** + * maciePutClassificationExportConfigurationFunction - lambda handler + * + * @param event + * @returns + */ +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + Status: string | undefined; + StatusCode: number | undefined; + } + | undefined +> { + const region = event.ResourceProperties['region']; + const bucketName = event.ResourceProperties['bucketName']; + const keyPrefix = event.ResourceProperties['keyPrefix']; + const kmsKeyArn = event.ResourceProperties['kmsKeyArn']; + + const macie2Client = new AWS.Macie2({ region: region }); + + switch (event.RequestType) { + case 'Create': + case 'Update': + if (!(await isMacieEnable(macie2Client))) { + console.log('start enable of macie'); + await throttlingBackOff(() => macie2Client.enableMacie({ status: 'ENABLED' }).promise()); + } + + await throttlingBackOff(() => + macie2Client + .putClassificationExportConfiguration({ + configuration: { + s3Destination: { + bucketName: bucketName, + keyPrefix: keyPrefix, + kmsKeyArn: kmsKeyArn, + }, + }, + }) + .promise(), + ); + return { Status: 'Success', StatusCode: 200 }; + + case 'Delete': + return { Status: 'Success', StatusCode: 200 }; + } +} + +/** + * Function to check if macie is enabled + * @param macie2Client + */ +async function isMacieEnable(macie2Client: AWS.Macie2): Promise { + try { + const response = await throttlingBackOff(() => macie2Client.getMacieSession({}).promise()); + return response.status === 'ENABLED'; + } catch ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + e: any + ) { + if ( + // SDKv2 Error Structure + e.code === 'ResourceConflictException' || + // SDKv3 Error Structure + e.name === 'ResourceConflictException' + ) { + console.warn(e.name + ': ' + e.message); + return false; + } + throw new Error(`Macie enable issue error message - ${e}`); + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-macie/put-export-config-classification/package.json b/source/packages/@aws-accelerator/constructs/lib/aws-macie/put-export-config-classification/package.json new file mode 100644 index 000000000..2c7e8d801 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-macie/put-export-config-classification/package.json @@ -0,0 +1,45 @@ +{ + "name": "@aws-accelerator/constructs-aws-macie-put-export-config-classification", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --outfile=./dist/index.js --platform=node --target=node14 --external:aws-sdk index.ts", + "test": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "esbuild": "0.14.2", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2" + }, + "peerDependencies": { + "eslint": "8.4.1" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1001.0" + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-macie/put-export-config-classification/tsconfig.json b/source/packages/@aws-accelerator/constructs/lib/aws-macie/put-export-config-classification/tsconfig.json new file mode 100644 index 000000000..272282f1f --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-macie/put-export-config-classification/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} \ No newline at end of file diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-networkfirewall/firewall.ts b/source/packages/@aws-accelerator/constructs/lib/aws-networkfirewall/firewall.ts new file mode 100644 index 000000000..b226af8d0 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-networkfirewall/firewall.ts @@ -0,0 +1,156 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +import { GetNetworkFirewallEndpoint } from './get-network-firewall-endpoint'; + +interface INetworkFirewall extends cdk.IResource { + /** + * The unique IDs of the firewall endpoints for all of the subnets that you attached to the firewall. + */ + readonly endpointIds: string[]; + + /** + * The Amazon Resource Name (ARN) of the firewall. + */ + readonly firewallArn: string; + + /** + * The ID of the firewall. + */ + readonly firewallId: string; + + /** + * The name of the policy. + */ + readonly firewallName: string; +} + +interface NetworkFirewallProps { + /** + * The Amazon Resource Name (ARN) of the firewall policy. + */ + readonly firewallPolicyArn: string; + + /** + * The descriptive name of the firewall. + */ + readonly name: string; + + /** + * The subnets that Network Firewall is using for the firewall. + */ + readonly subnets: string[]; + + /** + * The unique identifier of the VPC where the firewall is in use. + */ + readonly vpcId: string; + + /** + * A flag indicating whether it is possible to delete the firewall. + */ + readonly deleteProtection?: boolean; + + /** + * A description of the firewall. + */ + readonly description?: string; + + /** + * A setting indicating whether the firewall is protected against a change to the firewall policy association. + */ + readonly firewallPolicyChangeProtection?: boolean; + + /** + * A setting indicating whether the firewall is protected against changes to the subnet associations. + */ + readonly subnetChangeProtection?: boolean; + + /** + * An optional list of CloudFormation tags. + */ + readonly tags?: cdk.CfnTag[]; +} + +export class NetworkFirewall extends cdk.Resource implements INetworkFirewall { + public readonly endpointIds: string[]; + public readonly firewallArn: string; + public readonly firewallId: string; + public readonly firewallName: string; + private subnetMapping: cdk.aws_networkfirewall.CfnFirewall.SubnetMappingProperty[]; + + constructor(scope: Construct, id: string, props: NetworkFirewallProps) { + super(scope, id); + + // Set initial properties + this.firewallName = props.name; + this.subnetMapping = props.subnets.map(item => { + return { subnetId: item }; + }); + + // Set name tag + props.tags?.push({ key: 'Name', value: this.firewallName }); + + const resource = new cdk.aws_networkfirewall.CfnFirewall(this, 'Resource', { + firewallName: this.firewallName, + firewallPolicyArn: props.firewallPolicyArn, + subnetMappings: this.subnetMapping, + vpcId: props.vpcId, + deleteProtection: props.deleteProtection, + description: props.description, + firewallPolicyChangeProtection: props.firewallPolicyChangeProtection, + subnetChangeProtection: props.subnetChangeProtection, + tags: props.tags, + }); + + // Set remaining properties + this.endpointIds = resource.attrEndpointIds; + this.firewallArn = resource.ref; + this.firewallId = resource.attrFirewallId; + } + + public addLogging(config: cdk.aws_networkfirewall.CfnLoggingConfiguration.LoggingConfigurationProperty) { + new cdk.aws_networkfirewall.CfnLoggingConfiguration(this, 'LoggingConfig', { + firewallArn: this.firewallArn, + loggingConfiguration: config, + }); + } + + public addNetworkFirewallRoute(options: { + id: string; + destination: string; + endpointAz: string; + firewallArn: string; + kmsKey: cdk.aws_kms.Key; + logRetention: number; + routeTableId: string; + }): void { + // Get endpoint ID from custom resource + const endpointId = new GetNetworkFirewallEndpoint(this, `${options.id}Endpoint`, { + endpointAz: options.endpointAz, + firewallArn: options.firewallArn, + kmsKey: options.kmsKey, + logRetentionInDays: options.logRetention, + region: cdk.Stack.of(this).region, + }).endpointId; + + new cdk.aws_ec2.CfnRoute(this, options.id, { + routeTableId: options.routeTableId, + destinationCidrBlock: options.destination, + vpcEndpointId: endpointId, + }); + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-networkfirewall/get-network-firewall-endpoint.ts b/source/packages/@aws-accelerator/constructs/lib/aws-networkfirewall/get-network-firewall-endpoint.ts new file mode 100644 index 000000000..ac255eece --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-networkfirewall/get-network-firewall-endpoint.ts @@ -0,0 +1,97 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import * as path from 'path'; + +interface IGetNetworkFirewallEndpoint extends cdk.IResource { + /** + * The ID of the endpoint + */ + readonly endpointId: string; +} + +interface GetNetworkFirewallEndpointProps { + /** + * The AZ the endpoint is located in + */ + readonly endpointAz: string; + + /** + * The ARN of the associated Network Firewall + */ + readonly firewallArn: string; + + /** + * Custom resource lambda log group encryption key + */ + readonly kmsKey: cdk.aws_kms.Key; + + /** + * Custom resource lambda log retention in days + */ + readonly logRetentionInDays: number; + + /** + * The region of the Network Firewall + */ + readonly region: string; +} + +export class GetNetworkFirewallEndpoint extends cdk.Resource implements IGetNetworkFirewallEndpoint { + public readonly endpointId: string; + + constructor(scope: Construct, id: string, props: GetNetworkFirewallEndpointProps) { + super(scope, id); + + const provider = cdk.CustomResourceProvider.getOrCreateProvider(this, 'Custom::GetNetworkFirewallEndpoint', { + codeDirectory: path.join(__dirname, 'get-network-firewall-endpoint/dist'), + runtime: cdk.CustomResourceProviderRuntime.NODEJS_14_X, + policyStatements: [ + { + Effect: 'Allow', + Action: ['network-firewall:DescribeFirewall'], + Resource: '*', + }, + ], + }); + + const resource = new cdk.CustomResource(this, 'Resource', { + resourceType: 'Custom::GetNetworkFirewallEndpoint', + serviceToken: provider.serviceToken, + properties: { + endpointAz: props.endpointAz, + firewallArn: props.firewallArn, + region: props.region, + }, + }); + + /** + * Singleton pattern to define the log group for the singleton function + * in the stack + */ + const stack = cdk.Stack.of(scope); + const logGroup = + (stack.node.tryFindChild(`${provider.node.id}LogGroup`) as cdk.aws_logs.LogGroup) ?? + new cdk.aws_logs.LogGroup(stack, `${provider.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${(provider.node.findChild('Handler') as cdk.aws_lambda.CfnFunction).ref}`, + retention: props.logRetentionInDays, + encryptionKey: props.kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + resource.node.addDependency(logGroup); + + this.endpointId = resource.ref; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-networkfirewall/get-network-firewall-endpoint/index.ts b/source/packages/@aws-accelerator/constructs/lib/aws-networkfirewall/get-network-firewall-endpoint/index.ts new file mode 100644 index 000000000..98d46af8d --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-networkfirewall/get-network-firewall-endpoint/index.ts @@ -0,0 +1,69 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as AWS from 'aws-sdk'; + +import { throttlingBackOff } from '@aws-accelerator/utils'; + +AWS.config.logger = console; + +/** + * get-network-firewall-endpoint - lambda handler + * + * @param event + * @returns + */ + +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + PhysicalResourceId: string | undefined; + Status: string; + } + | undefined +> { + // Set variables passed in event + const endpointAz: string = event.ResourceProperties['endpointAz']; + const firewallArn: string = event.ResourceProperties['firewallArn']; + const region: string = event.ResourceProperties['region']; + const nfwClient = new AWS.NetworkFirewall({ region: region }); + + switch (event.RequestType) { + case 'Create': + case 'Update': + let endpointId: string | undefined = undefined; + const response = await throttlingBackOff(() => + nfwClient.describeFirewall({ FirewallArn: firewallArn }).promise(), + ); + + // Check for endpoint in specified AZ + if (response.FirewallStatus?.SyncStates) { + endpointId = response.FirewallStatus?.SyncStates[endpointAz].Attachment?.EndpointId; + } + + if (endpointId) { + return { + PhysicalResourceId: endpointId, + Status: 'SUCCESS', + }; + } else { + throw new Error(`Unable to locate Network Firewall endpoint in AZ ${endpointAz}`); + } + + case 'Delete': + // Do nothing + return { + PhysicalResourceId: event.PhysicalResourceId, + Status: 'SUCCESS', + }; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-networkfirewall/get-network-firewall-endpoint/package.json b/source/packages/@aws-accelerator/constructs/lib/aws-networkfirewall/get-network-firewall-endpoint/package.json new file mode 100644 index 000000000..4b44c861c --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-networkfirewall/get-network-firewall-endpoint/package.json @@ -0,0 +1,45 @@ +{ + "name": "@aws-accelerator/constructs-aws-networkfirewall-get-network-firewall-endpoint", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --outfile=./dist/index.js --platform=node --target=node14 --external:aws-sdk index.ts", + "test": "", + "testreport": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2" + }, + "peerDependencies": { + "eslint": "8.4.1" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1001.0" + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-networkfirewall/get-network-firewall-endpoint/tsconfig.json b/source/packages/@aws-accelerator/constructs/lib/aws-networkfirewall/get-network-firewall-endpoint/tsconfig.json new file mode 100644 index 000000000..ebcb98a79 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-networkfirewall/get-network-firewall-endpoint/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-networkfirewall/policy.ts b/source/packages/@aws-accelerator/constructs/lib/aws-networkfirewall/policy.ts new file mode 100644 index 000000000..23301ff21 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-networkfirewall/policy.ts @@ -0,0 +1,160 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +import { NfwRuleSourceCustomActionConfig } from '@aws-accelerator/config'; + +interface INetworkFirewallPolicy extends cdk.IResource { + /** + * The Amazon Resource Name (ARN) of the policy. + */ + readonly policyArn: string; + + /** + * The ID of the policy. + */ + readonly policyId: string; + + /** + * The name of the policy. + */ + readonly policyName: string; +} + +interface StatefulRuleGroupReference { + resourceArn: string; + priority?: number; +} + +interface StatelessRuleGroupReference { + priority: number; + resourceArn: string; +} + +export interface FirewallPolicyProperty { + statelessDefaultActions: string[]; + statelessFragmentDefaultActions: string[]; + statefulDefaultActions?: string[]; + statefulEngineOptions?: string; + statefulRuleGroupReferences?: StatefulRuleGroupReference[]; + statelessCustomActions?: NfwRuleSourceCustomActionConfig[]; + statelessRuleGroupReferences?: StatelessRuleGroupReference[]; +} + +interface NetworkFirewallPolicyProps { + /** + * The traffic filtering behavior of a firewall policy, defined in a collection of stateless and stateful rule groups and other settings. + */ + readonly firewallPolicy: FirewallPolicyProperty; + + /** + * The descriptive name of the firewall policy. + */ + readonly name: string; + + /** + * A description of the firewall policy. + */ + readonly description?: string; + + /** + * An optional list of CloudFormation tags. + */ + readonly tags?: cdk.CfnTag[]; +} + +export class NetworkFirewallPolicy extends cdk.Resource implements INetworkFirewallPolicy { + public readonly policyArn: string; + public readonly policyId: string; + public readonly policyName: string; + private firewallPolicy: cdk.aws_networkfirewall.CfnFirewallPolicy.FirewallPolicyProperty; + private customActions?: cdk.aws_networkfirewall.CfnFirewallPolicy.CustomActionProperty[]; + private statefulOptions?: cdk.aws_networkfirewall.CfnFirewallPolicy.StatefulEngineOptionsProperty; + + constructor(scope: Construct, id: string, props: NetworkFirewallPolicyProps) { + super(scope, id); + + // Set initial properties + this.policyName = props.name; + + // Transform properties as necessary + if (props.firewallPolicy.statelessCustomActions) { + this.transformCustom(props.firewallPolicy); + } + if (props.firewallPolicy.statefulEngineOptions) { + this.transformEngineOptions(props.firewallPolicy); + } + + // Set firewall policy property + this.firewallPolicy = { + statelessDefaultActions: props.firewallPolicy.statelessDefaultActions, + statelessFragmentDefaultActions: props.firewallPolicy.statelessFragmentDefaultActions, + statefulDefaultActions: props.firewallPolicy.statefulDefaultActions, + statefulEngineOptions: this.statefulOptions, + statefulRuleGroupReferences: props.firewallPolicy.statefulRuleGroupReferences, + statelessCustomActions: this.customActions, + statelessRuleGroupReferences: props.firewallPolicy.statelessRuleGroupReferences, + }; + + // Set name tag + props.tags?.push({ key: 'Name', value: this.policyName }); + + const resource = new cdk.aws_networkfirewall.CfnFirewallPolicy(this, 'Resource', { + firewallPolicy: this.firewallPolicy, + firewallPolicyName: this.policyName, + description: props.description, + tags: props.tags, + }); + + this.policyArn = resource.ref; + this.policyId = resource.attrFirewallPolicyId; + } + + /** + * Transform custom actions to conform with L1 construct. + * + * @param props + */ + private transformCustom(props: FirewallPolicyProperty) { + const property = props.statelessCustomActions; + this.customActions = []; + + for (const action of property ?? []) { + this.customActions.push({ + actionDefinition: { + publishMetricAction: { + dimensions: action.actionDefinition.publishMetricAction.dimensions.map(item => { + return { value: item }; + }), + }, + }, + actionName: action.actionName, + }); + } + } + + /** + * Transform engine options to conform with L1 construct. + * + * @param props + */ + private transformEngineOptions(props: FirewallPolicyProperty) { + const property = props.statefulEngineOptions; + + if (property) { + this.statefulOptions = { ruleOrder: property }; + } + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-networkfirewall/rule-group.ts b/source/packages/@aws-accelerator/constructs/lib/aws-networkfirewall/rule-group.ts new file mode 100644 index 000000000..2f67758d8 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-networkfirewall/rule-group.ts @@ -0,0 +1,216 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +import { NfwRuleGroupRuleConfig } from '@aws-accelerator/config'; + +interface INetworkFirewallRuleGroup extends cdk.IResource { + /** + * The Amazon Resource Name (ARN) of the rule group. + */ + readonly groupArn: string; + + /** + * The ID of the rule group. + */ + readonly groupId: string; + + /** + * The name of the rule group. + */ + readonly groupName: string; +} + +interface NetworkFirewallRuleGroupProps { + /** + * The maximum operating resources that this rule group can use. + */ + readonly capacity: number; + + /** + * The name of the rule group. + */ + readonly name: string; + + /** + * Indicates whether the rule group is stateless or stateful. + */ + readonly type: string; + + /** + * A description of the rule group. + */ + readonly description?: string; + + /** + * An object that defines the rule group rules. + */ + readonly ruleGroup?: NfwRuleGroupRuleConfig; + + /** + * An optional list of CloudFormation tags. + */ + readonly tags?: cdk.CfnTag[]; +} + +export class NetworkFirewallRuleGroup extends cdk.Resource implements INetworkFirewallRuleGroup { + public readonly groupArn: string; + public readonly groupId: string; + public readonly groupName: string; + private ruleGroup?: cdk.aws_networkfirewall.CfnRuleGroup.RuleGroupProperty; + private statelessRules?: cdk.aws_networkfirewall.CfnRuleGroup.StatelessRuleProperty[]; + private customActions?: cdk.aws_networkfirewall.CfnRuleGroup.CustomActionProperty[]; + private ruleVariables?: cdk.aws_networkfirewall.CfnRuleGroup.RuleVariablesProperty; + private ruleOptions?: cdk.aws_networkfirewall.CfnRuleGroup.StatefulRuleOptionsProperty; + + constructor(scope: Construct, id: string, props: NetworkFirewallRuleGroupProps) { + super(scope, id); + + // Set initial properties + this.groupName = props.name; + + // Transform properties as necessary + if (props.ruleGroup) { + let statelessAndCustom = undefined; + if (props.ruleGroup.rulesSource.statelessRulesAndCustomActions) { + this.transformStatelessCustom(props.ruleGroup); + statelessAndCustom = { + statelessRules: this.statelessRules!, + customActions: this.customActions, + }; + } + if (props.ruleGroup.ruleVariables) { + this.transformRuleVariables(props.ruleGroup); + } + if (props.ruleGroup.statefulRuleOptions) { + this.transformRuleOptions(props.ruleGroup); + } + + // Set rule group property + this.ruleGroup = { + rulesSource: { + rulesSourceList: props.ruleGroup.rulesSource.rulesSourceList, + rulesString: props.ruleGroup.rulesSource.rulesString, + statefulRules: props.ruleGroup.rulesSource.statefulRules, + statelessRulesAndCustomActions: statelessAndCustom, + }, + ruleVariables: this.ruleVariables, + statefulRuleOptions: this.ruleOptions, + }; + } + + // Set name tag + props.tags?.push({ key: 'Name', value: this.groupName }); + + const resource = new cdk.aws_networkfirewall.CfnRuleGroup(this, 'Resource', { + capacity: props.capacity, + ruleGroupName: this.groupName, + type: props.type, + description: props.description, + ruleGroup: this.ruleGroup, + tags: props.tags, + }); + + this.groupArn = resource.ref; + this.groupId = resource.attrRuleGroupId; + } + + /** + * Transform stateless and custom rule group policies to conform with L1 construct. + * + * @param props + */ + private transformStatelessCustom(props: NfwRuleGroupRuleConfig) { + const property = props.rulesSource.statelessRulesAndCustomActions; + this.statelessRules = []; + this.customActions = []; + + // Push stateless rules + for (const rule of property?.statelessRules ?? []) { + this.statelessRules.push({ + priority: rule.priority, + ruleDefinition: { + actions: rule.ruleDefinition.actions, + matchAttributes: { + destinationPorts: rule.ruleDefinition.matchAttributes.destinationPorts, + destinations: rule.ruleDefinition.matchAttributes.destinations.map(item => { + return { addressDefinition: item }; + }), + protocols: rule.ruleDefinition.matchAttributes.protocols, + sourcePorts: rule.ruleDefinition.matchAttributes.sourcePorts, + sources: rule.ruleDefinition.matchAttributes.destinations.map(item => { + return { addressDefinition: item }; + }), + tcpFlags: rule.ruleDefinition.matchAttributes.tcpFlags, + }, + }, + }); + } + + // Push custom actions + for (const action of property?.customActions ?? []) { + this.customActions.push({ + actionDefinition: { + publishMetricAction: { + dimensions: action.actionDefinition.publishMetricAction.dimensions.map(item => { + return { value: item }; + }), + }, + }, + actionName: action.actionName, + }); + } + } + + /** + * Transform rule variables to conform with L1 construct. + * + * @param props + */ + private transformRuleVariables(props: NfwRuleGroupRuleConfig) { + const property = props.ruleVariables; + + if (property) { + // Create ipset object + const ipSet: { [key: string]: { definition: string[] } } = {}; + const ipSetKey = property.ipSets.name; + ipSet[ipSetKey] = { definition: property.ipSets.definition }; + + // Create portset object + const portSet: { [key: string]: { definition: string[] } } = {}; + const portSetKey = property.portSets.name; + portSet[portSetKey] = { definition: property.portSets.definition }; + + // Instantiate ruleVariables object + this.ruleVariables = { + ipSets: ipSet, + portSets: portSet, + }; + } + } + + /** + * Transform rule options to conform with L1 construct. + * + * @param props + */ + private transformRuleOptions(props: NfwRuleGroupRuleConfig) { + const property = props.statefulRuleOptions; + + if (property) { + this.ruleOptions = { ruleOrder: property }; + } + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-organizations/account.ts b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/account.ts new file mode 100644 index 000000000..6e44d5a03 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/account.ts @@ -0,0 +1,105 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +const path = require('path'); + +export interface IAccount extends cdk.IResource { + readonly accountId: string; + readonly assumeRoleName: string; +} + +/** + * Account properties + */ +export interface AccountProps { + readonly accountId: string; + readonly assumeRoleName: string; + /** + * Custom resource lambda log group encryption key + */ + readonly kmsKey: cdk.aws_kms.Key; + /** + * Custom resource lambda log retention in days + */ + readonly logRetentionInDays: number; +} + +/** + * Class to initialize an Organizations Account + */ +export class Account extends cdk.Resource implements IAccount { + public readonly accountId: string; + public readonly assumeRoleName: string; + + constructor(scope: Construct, id: string, props: AccountProps) { + super(scope, id); + + this.accountId = props.accountId; + this.assumeRoleName = props.assumeRoleName; + + const ENROLL_ACCOUNT_TYPE = 'Custom::InviteAccountToOrganization'; + + const provider = cdk.CustomResourceProvider.getOrCreateProvider(this, ENROLL_ACCOUNT_TYPE, { + codeDirectory: path.join(__dirname, 'invite-account-to-organization/dist'), + runtime: cdk.CustomResourceProviderRuntime.NODEJS_14_X, + policyStatements: [ + { + Effect: 'Allow', + Action: [ + 'organizations:AcceptHandshake', + 'organizations:ListAccounts', + 'organizations:InviteAccountToOrganization', + 'organizations:MoveAccount', + 'sts:AssumeRole', + ], + Resource: '*', + }, + ], + }); + + const resource = new cdk.CustomResource(this, 'Resource', { + resourceType: ENROLL_ACCOUNT_TYPE, + serviceToken: provider.serviceToken, + properties: { + accountId: props.accountId, + partition: cdk.Aws.PARTITION, + roleArn: cdk.Stack.of(this).formatArn({ + service: 'iam', + region: '', + account: props.accountId, + resource: 'role', + arnFormat: cdk.ArnFormat.SLASH_RESOURCE_NAME, + resourceName: props.assumeRoleName, + }), + }, + }); + + /** + * Singleton pattern to define the log group for the singleton function + * in the stack + */ + const stack = cdk.Stack.of(scope); + const logGroup = + (stack.node.tryFindChild(`${provider.node.id}LogGroup`) as cdk.aws_logs.LogGroup) ?? + new cdk.aws_logs.LogGroup(stack, `${provider.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${(provider.node.findChild('Handler') as cdk.aws_lambda.CfnFunction).ref}`, + retention: props.logRetentionInDays, + encryptionKey: props.kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + resource.node.addDependency(logGroup); + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-organizations/attach-policy/index.ts b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/attach-policy/index.ts new file mode 100644 index 000000000..b1bf2a3ed --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/attach-policy/index.ts @@ -0,0 +1,110 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { throttlingBackOff } from '@aws-accelerator/utils'; +import * as AWS from 'aws-sdk'; +AWS.config.logger = console; + +/** + * attach-policy - lambda handler + * + * @param event + * @returns + */ +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + PhysicalResourceId: string | undefined; + Status: string; + } + | undefined +> { + const policyId: string = event.ResourceProperties['policyId']; + const targetId: string = event.ResourceProperties['targetId'] ?? undefined; + const type: string = event.ResourceProperties['type']; + const partition: string = event.ResourceProperties['partition']; + + let organizationsClient: AWS.Organizations; + if (partition === 'aws-us-gov') { + organizationsClient = new AWS.Organizations({ region: 'us-gov-west-1' }); + } else { + organizationsClient = new AWS.Organizations({ region: 'us-east-1' }); + } + + switch (event.RequestType) { + case 'Create': + case 'Update': + // + // Check if already exists, update and return the ID + // + let nextToken: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => + organizationsClient + .listPoliciesForTarget({ Filter: type, TargetId: targetId, NextToken: nextToken }) + .promise(), + ); + for (const policy of page.Policies ?? []) { + if (policy.Id === policyId) { + console.log('Policy already attached'); + return { + PhysicalResourceId: `${policyId}_${targetId}`, + Status: 'SUCCESS', + }; + } + } + nextToken = page.NextToken; + } while (nextToken); + + // + // Create if not found + // + await throttlingBackOff(() => + organizationsClient.attachPolicy({ PolicyId: policyId, TargetId: targetId }).promise(), + ); + + return { + PhysicalResourceId: `${policyId}_${targetId}`, + Status: 'SUCCESS', + }; + + case 'Delete': + // + // Detach policy, let CDK manage where it's deployed, + // + + // do not remove FullAWSAccess + if (policyId !== 'p-FullAWSAccess') { + let nextToken: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => + organizationsClient + .listPoliciesForTarget({ Filter: type, TargetId: targetId, NextToken: nextToken }) + .promise(), + ); + for (const policy of page.Policies ?? []) { + if (policy.Id === policyId) { + await throttlingBackOff(() => + organizationsClient.detachPolicy({ PolicyId: policyId, TargetId: targetId }).promise(), + ); + } + } + nextToken = page.NextToken; + } while (nextToken); + } + + return { + PhysicalResourceId: event.PhysicalResourceId, + Status: 'SUCCESS', + }; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-organizations/attach-policy/package.json b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/attach-policy/package.json new file mode 100644 index 000000000..0999532a9 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/attach-policy/package.json @@ -0,0 +1,46 @@ +{ + "name": "@aws-accelerator/constructs-aws-organizations-attach-policy", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --outfile=./dist/index.js --platform=node --target=node14 --external:aws-sdk index.ts", + "test": "", + "testreport": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "esbuild": "0.14.2", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2" + }, + "peerDependencies": { + "eslint": "8.4.1" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1001.0" + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-organizations/attach-policy/tsconfig.json b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/attach-policy/tsconfig.json new file mode 100644 index 000000000..ebcb98a79 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/attach-policy/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-accounts-status/index.ts b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-accounts-status/index.ts new file mode 100644 index 000000000..80854f1d5 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-accounts-status/index.ts @@ -0,0 +1,306 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +/** + * aws-organization-create-accounts - lambda handler + * + * @param event + * @returns + */ + +import * as AWS from 'aws-sdk'; +import { throttlingBackOff } from '@aws-accelerator/utils'; +import { CreateAccountResponse } from 'aws-sdk/clients/organizations'; + +const documentClient = new AWS.DynamoDB.DocumentClient(); +const newOrgAccountsTableName = process.env['NewOrgAccountsTableName'] ?? ''; +const govCloudAccountMappingTableName = process.env['GovCloudAccountMappingTableName'] ?? ''; +const accountRoleName = process.env['AccountRoleName']; + +interface AccountConfig { + name: string; + description: string; + email: string; + enableGovCloud: string; + organizationalUnitId: string; + createRequestId?: string | undefined; +} + +type AccountConfigs = Array; + +const organizationsClient = new AWS.Organizations({ region: 'us-east-1' }); + +//eslint-disable-next-line @typescript-eslint/no-explicit-any +export async function handler(event: any): Promise< + | { + IsComplete: boolean; + } + | undefined +> { + console.log(event); + // get a single accountConfig from table and attempt to create + // if no record is returned then all new accounts are provisioned + try { + const accountToAdd = await getSingleAccountConfigFromTable(); + if (accountToAdd.length === 0) { + console.log('Finished adding accounts'); + return { + IsComplete: true, + }; + } + + const singleAccountToAdd = accountToAdd[0]; + console.log(`enablegovcloud value: ${singleAccountToAdd.enableGovCloud}`); + let createAccountResponse: CreateAccountResponse; + // if the createRequestId is empty then we need to create the account + if (singleAccountToAdd.createRequestId === '' || singleAccountToAdd.createRequestId === undefined) { + if (singleAccountToAdd.enableGovCloud == 'true' || singleAccountToAdd.enableGovCloud) { + createAccountResponse = await createGovCloudAccount(singleAccountToAdd.email, singleAccountToAdd.name); + } else { + createAccountResponse = await createOrganizationAccount(singleAccountToAdd.email, singleAccountToAdd.name); + } + switch (createAccountResponse.CreateAccountStatus?.State) { + case 'IN_PROGRESS': + console.log(`Initiated account creation for ${accountToAdd[0].email}`); + singleAccountToAdd.createRequestId = createAccountResponse.CreateAccountStatus.Id; + const updateAccountConfigResponse = await updateAccountConfig(singleAccountToAdd); + if (!updateAccountConfigResponse) { + throw new Error('Unable to update DynamoDB account record with request id'); + } else { + return { + IsComplete: false, + }; + } + case 'SUCCEEDED': + if (createAccountResponse.CreateAccountStatus.GovCloudAccountId) { + console.log( + `GovCloud account created with id ${createAccountResponse.CreateAccountStatus.GovCloudAccountId}`, + ); + await saveGovCloudAccountMapping( + createAccountResponse.CreateAccountStatus.AccountId!, + createAccountResponse.CreateAccountStatus.GovCloudAccountId, + createAccountResponse.CreateAccountStatus.AccountName!, + ); + } + console.log( + `Account with id ${createAccountResponse.CreateAccountStatus.AccountId} was created for email ${singleAccountToAdd.email}`, + ); + await moveAccountToOrgIdFromRoot( + createAccountResponse.CreateAccountStatus.AccountId!, + singleAccountToAdd.organizationalUnitId, + ); + await deleteSingleAccountConfigFromTable(singleAccountToAdd.email); + break; + default: + throw new Error( + `Could not create account ${singleAccountToAdd.email}. Response state: ${createAccountResponse.CreateAccountStatus?.State}. Failure reason: ${createAccountResponse.CreateAccountStatus?.FailureReason}`, + ); + } + } else { + // check status of account creation + const createAccountStatusResponse = await getAccountCreationStatus(singleAccountToAdd.createRequestId); + switch (createAccountStatusResponse.CreateAccountStatus?.State) { + case 'IN_PROGRESS': + console.log(`Account is still being created`); + return { + IsComplete: false, + }; + case 'SUCCEEDED': + console.log(`Account with id ${createAccountStatusResponse.CreateAccountStatus?.AccountId} is complete`); + if (createAccountStatusResponse.CreateAccountStatus.GovCloudAccountId) { + console.log(createAccountStatusResponse.CreateAccountStatus.GovCloudAccountId); + await saveGovCloudAccountMapping( + createAccountStatusResponse.CreateAccountStatus.AccountId!, + createAccountStatusResponse.CreateAccountStatus.GovCloudAccountId, + singleAccountToAdd.name, + ); + } + await moveAccountToOrgIdFromRoot( + createAccountStatusResponse.CreateAccountStatus.AccountId!, + singleAccountToAdd.organizationalUnitId, + ); + await deleteSingleAccountConfigFromTable(singleAccountToAdd.email); + break; + default: + throw new Error( + `Could not create account ${singleAccountToAdd.email}. Response state: ${createAccountStatusResponse.CreateAccountStatus?.State}, Failure reason: ${createAccountStatusResponse.CreateAccountStatus?.FailureReason}`, + ); + } + } + return { + IsComplete: false, + }; + } catch (e) { + console.log(e); + console.log(`Create accounts failed. Deleting pending account creation records`); + await deleteAllRecordsFromTable(newOrgAccountsTableName); + throw new Error(`Account creation failed. ${e}`); + } +} + +async function getSingleAccountConfigFromTable(): Promise { + const accountToAdd: AccountConfigs = []; + const scanParams = { + TableName: newOrgAccountsTableName, + Limit: 1, + }; + + const response = await throttlingBackOff(() => documentClient.scan(scanParams).promise()); + + console.log(`getSingleAccount response ${JSON.stringify(response)}`); + if (response.Items?.length ?? 0 > 0) { + const account: AccountConfig = JSON.parse(response.Items![0]['accountConfig']); + accountToAdd.push(account); + console.log(`Account to add ${JSON.stringify(accountToAdd)}`); + } + return accountToAdd; +} + +async function deleteSingleAccountConfigFromTable(accountToDeleteEmail: string): Promise { + const deleteParams = { + TableName: newOrgAccountsTableName, + Key: { + accountEmail: accountToDeleteEmail, + }, + }; + const response = await throttlingBackOff(() => documentClient.delete(deleteParams).promise()); + if (response.$response.httpResponse.statusCode === 200) { + return true; + } else { + console.log(response); + return false; + } +} + +async function createOrganizationAccount( + accountEmail: string, + accountName: string, +): Promise { + const createAccountsParams = { + AccountName: accountName, + Email: accountEmail, + RoleName: accountRoleName, + }; + const createAccountResponse = await throttlingBackOff(() => + organizationsClient.createAccount(createAccountsParams).promise(), + ); + console.log(createAccountResponse); + return createAccountResponse; +} + +async function createGovCloudAccount( + accountEmail: string, + accountName: string, +): Promise { + const createAccountsParams = { + AccountName: accountName, + Email: accountEmail, + RoleName: accountRoleName, + }; + const createAccountResponse = await throttlingBackOff(() => + organizationsClient.createGovCloudAccount(createAccountsParams).promise(), + ); + console.log(createAccountResponse); + return createAccountResponse; +} + +async function getAccountCreationStatus( + requestId: string, +): Promise { + const response = await throttlingBackOff(() => + organizationsClient.describeCreateAccountStatus({ CreateAccountRequestId: requestId }).promise(), + ); + return response; +} + +// TODO: Fix use update +async function updateAccountConfig(accountConfig: AccountConfig): Promise { + const params = { + TableName: newOrgAccountsTableName, + Item: { + accountEmail: accountConfig.email, + accountConfig: JSON.stringify(accountConfig), + }, + }; + const response = await throttlingBackOff(() => documentClient.put(params).promise()); + if (response.$response.httpResponse.statusCode === 200) { + return true; + } else { + console.log(response); + return false; + } +} + +async function moveAccountToOrgIdFromRoot(accountId: string, orgId: string): Promise { + const roots = await throttlingBackOff(() => organizationsClient.listRoots({}).promise()); + const rootOrg = roots.Roots?.find(item => item.Name === 'Root'); + const response = await throttlingBackOff(() => + organizationsClient + .moveAccount({ + AccountId: accountId, + DestinationParentId: orgId, + SourceParentId: rootOrg!.Id!, + }) + .promise(), + ); + if (response.$response.httpResponse.statusCode === 200) { + console.log(`Moved account ${accountId} to OU.`); + return true; + } else { + console.log( + `Failed to move account ${accountId} to OU. Move request status: ${response.$response.httpResponse.statusMessage}`, + ); + } + return false; +} + +async function saveGovCloudAccountMapping( + commercialAccountId: string, + govCloudAccountId: string, + accountName: string, +): Promise { + const params = { + TableName: govCloudAccountMappingTableName, + Item: { + commercialAccountId: commercialAccountId, + govCloudAccountId: govCloudAccountId, + accountName: accountName, + }, + }; + const response = await throttlingBackOff(() => documentClient.put(params).promise()); + if (response.$response.httpResponse.statusCode === 200) { + return true; + } else { + console.log(response); + return false; + } +} + +async function deleteAllRecordsFromTable(tableName: string) { + const params = { + TableName: tableName, + ProjectionExpression: 'accountEmail', + }; + const response = await documentClient.scan(params).promise(); + if (response.Items) { + for (const item of response.Items) { + console.log(item['accountEmail']); + const params = { + TableName: tableName, + Key: { + accountEmail: item['accountEmail'], + }, + }; + await documentClient.delete(params).promise(); + } + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-accounts-status/package.json b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-accounts-status/package.json new file mode 100644 index 000000000..3648ee84a --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-accounts-status/package.json @@ -0,0 +1,46 @@ +{ + "name": "@aws-accelerator/constructs-aws-organizations-create-accounts-status", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --sourcemap --platform=node --target=node14 --external:aws-sdk --outfile=./dist/index.js index.ts", + "test": "", + "testreport": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2", + "ts-node": "10.4.0" + }, + "peerDependencies": { + "eslint": "8.4.1" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1001.0" + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-accounts-status/tsconfig.json b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-accounts-status/tsconfig.json new file mode 100644 index 000000000..ebcb98a79 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-accounts-status/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-accounts.ts b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-accounts.ts new file mode 100644 index 000000000..efdb2d380 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-accounts.ts @@ -0,0 +1,143 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { v4 as uuidv4 } from 'uuid'; +import { Construct } from 'constructs'; + +import path = require('path'); + +/** + * Organizations create accounts + */ +export interface CreateOrganizationAccountsProps { + readonly newOrgAccountsTable: cdk.aws_dynamodb.ITable; + readonly govCloudAccountMappingTable: cdk.aws_dynamodb.ITable | undefined; + readonly accountRoleName: string; + /** + * Custom resource lambda log group encryption key + */ + readonly kmsKey: cdk.aws_kms.Key; + /** + * Custom resource lambda log retention in days + */ + readonly logRetentionInDays: number; +} + +export class CreateOrganizationAccounts extends Construct { + readonly onEvent: cdk.aws_lambda.IFunction; + readonly isComplete: cdk.aws_lambda.IFunction; + readonly provider: cdk.custom_resources.Provider; + readonly id: string; + + constructor(scope: Construct, id: string, props: CreateOrganizationAccountsProps) { + super(scope, id); + + const CREATE_ORGANIZATION_ACCOUNTS_RESOURCE_TYPE = 'Custom::CreateOrganizationAccounts'; + + this.onEvent = new cdk.aws_lambda.Function(this, 'CreateOrganizationAccounts', { + code: cdk.aws_lambda.Code.fromAsset(path.join(__dirname, 'create-accounts/dist')), + runtime: cdk.aws_lambda.Runtime.NODEJS_14_X, + handler: 'index.handler', + timeout: cdk.Duration.seconds(30), + description: 'Create Organization Accounts OnEvent handler', + environmentEncryption: props.kmsKey, + }); + new cdk.aws_logs.LogGroup(this, `${this.onEvent.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${this.onEvent.functionName}`, + retention: props.logRetentionInDays, + encryptionKey: props.kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + const ddbPolicy = new cdk.aws_iam.PolicyStatement({ + sid: 'DynamoDb', + effect: cdk.aws_iam.Effect.ALLOW, + actions: ['dynamodb:Scan', 'dynamodb:GetItem', 'dynamodb:DeleteItem', 'dynamodb:PutItem'], + resources: [props.newOrgAccountsTable.tableArn], + }); + const ddbKmsPolicy = new cdk.aws_iam.PolicyStatement({ + sid: 'KMS', + effect: cdk.aws_iam.Effect.ALLOW, + actions: ['kms:Encrypt', 'kms:Decrypt', 'kms:GenerateDataKey*', 'kms:DescribeKey'], + resources: [props.newOrgAccountsTable.encryptionKey?.keyArn as string], + }); + const orgPolicy = new cdk.aws_iam.PolicyStatement({ + sid: 'Organizations', + effect: cdk.aws_iam.Effect.ALLOW, + actions: [ + 'organizations:CreateAccount', + 'organizations:CreateGovCloudAccount', + 'organizations:DescribeCreateAccountStatus', + 'organizations:ListRoots', + 'organizations:MoveAccount', + ], + resources: ['*'], + }); + + this.isComplete = new cdk.aws_lambda.Function(this, 'CreateOrganizationAccountStatus', { + code: cdk.aws_lambda.Code.fromAsset(path.join(__dirname, 'create-accounts-status/dist')), + runtime: cdk.aws_lambda.Runtime.NODEJS_14_X, + handler: 'index.handler', + timeout: cdk.Duration.minutes(5), + description: 'Create Organization Account isComplete handler', + environment: { + NewOrgAccountsTableName: props.newOrgAccountsTable.tableName, + GovCloudAccountMappingTableName: props.govCloudAccountMappingTable?.tableName || '', + AccountRoleName: props.accountRoleName, + }, + initialPolicy: [ddbPolicy, ddbKmsPolicy, orgPolicy], + environmentEncryption: props.kmsKey, + }); + new cdk.aws_logs.LogGroup(this, `${this.isComplete.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${this.isComplete.functionName}`, + retention: props.logRetentionInDays, + encryptionKey: props.kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + if (props.govCloudAccountMappingTable) { + const mappingTablePolicy = new cdk.aws_iam.PolicyStatement({ + sid: 'MappingDynamoDb', + effect: cdk.aws_iam.Effect.ALLOW, + actions: ['dynamodb:GetItem', 'dynamodb:PutItem'], + resources: [props.govCloudAccountMappingTable!.tableArn], + }); + const mappingTableKeyPolicy = new cdk.aws_iam.PolicyStatement({ + sid: 'MappingKMS', + effect: cdk.aws_iam.Effect.ALLOW, + actions: ['kms:Encrypt', 'kms:Decrypt', 'kms:GenerateDataKey*', 'kms:DescribeKey'], + resources: [props.govCloudAccountMappingTable.encryptionKey?.keyArn as string], + }); + this.isComplete.addToRolePolicy(mappingTablePolicy); + this.isComplete.addToRolePolicy(mappingTableKeyPolicy); + } + + this.provider = new cdk.custom_resources.Provider(this, 'CreateOrganizationAccountsProvider', { + onEventHandler: this.onEvent, + isCompleteHandler: this.isComplete, + queryInterval: cdk.Duration.seconds(15), + totalTimeout: cdk.Duration.hours(2), + }); + + const resource = new cdk.CustomResource(this, 'Resource', { + resourceType: CREATE_ORGANIZATION_ACCOUNTS_RESOURCE_TYPE, + serviceToken: this.provider.serviceToken, + properties: { + uuid: uuidv4(), // Generates a new UUID to force the resource to update + }, + }); + + this.id = resource.ref; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-accounts/index.ts b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-accounts/index.ts new file mode 100644 index 000000000..229fd6cff --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-accounts/index.ts @@ -0,0 +1,39 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +/** + * aws-organization-create-accounts - lambda handler + * + * @param event + * @returns + */ + +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + IsComplete: boolean; + } + | undefined +> { + switch (event.RequestType) { + case 'Create': + case 'Update': + return { + IsComplete: false, + }; + + case 'Delete': + // Do Nothing + return { + IsComplete: true, + }; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-accounts/package.json b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-accounts/package.json new file mode 100644 index 000000000..5991e647b --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-accounts/package.json @@ -0,0 +1,44 @@ +{ + "name": "@aws-accelerator/constructs-aws-organizations-create-accounts", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --sourcemap --platform=node --target=node14 --external:aws-sdk --outfile=./dist/index.js index.ts", + "test": "", + "testreport": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2" + }, + "peerDependencies": { + "eslint": "8.4.1" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0" + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-accounts/tsconfig.json b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-accounts/tsconfig.json new file mode 100644 index 000000000..ebcb98a79 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-accounts/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-organizational-units/index.ts b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-organizational-units/index.ts new file mode 100644 index 000000000..df7d4ee32 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-organizational-units/index.ts @@ -0,0 +1,255 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { throttlingBackOff } from '@aws-accelerator/utils'; +import * as AWS from 'aws-sdk'; +import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; +import { + DynamoDBDocumentClient, + UpdateCommand, + UpdateCommandInput, + paginateQuery, + DynamoDBDocumentPaginationConfiguration, +} from '@aws-sdk/lib-dynamodb'; +AWS.config.logger = console; + +let organizationsClient: AWS.Organizations; +const marshallOptions = { + convertEmptyValues: false, + //overriding default value of false + removeUndefinedValues: true, + convertClassInstanceToMap: false, +}; +const unmarshallOptions = { + wrapNumbers: false, +}; +const translateConfig = { marshallOptions, unmarshallOptions }; +const dynamodbClient1 = new DynamoDBClient({}); +const documentClient = DynamoDBDocumentClient.from(dynamodbClient1, translateConfig); +const paginationConfig: DynamoDBDocumentPaginationConfiguration = { + client: documentClient, + pageSize: 100, +}; + +type DDBItem = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any; +}; +type DDBItems = Array; + +/** + * create-organizational-units - lambda handler + * + * @param event + * @returns + */ +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + Status: string; + } + | undefined +> { + const configTableName = event.ResourceProperties['configTableName']; + const commitId = event.ResourceProperties['commitId']; + const controlTowerEnabled = event.ResourceProperties['controlTowerEnabled']; + const organizationsEnabled = event.ResourceProperties['organizationsEnabled']; + const partition = event.ResourceProperties['partition']; + + const organizationalUnitsToCreate: AWS.DynamoDB.DocumentClient.AttributeMap = []; + + switch (event.RequestType) { + case 'Create': + case 'Update': + if (organizationsEnabled == 'false' || controlTowerEnabled == 'true') { + return { + Status: 'SUCCESS', + }; + } + if (partition === 'aws-us-gov') { + organizationsClient = new AWS.Organizations({ region: 'us-gov-west-1' }); + } else { + organizationsClient = new AWS.Organizations({ region: 'us-east-1' }); + } + + //read config from table + const organizationalUnitList = await getConfigFromTable(configTableName, commitId); + console.log(`Organizational Units retrieved from config table: ${JSON.stringify(organizationalUnitList)}`); + + //build list of organizational units that need to be created + if (organizationalUnitList) { + for (const organizationalUnit of organizationalUnitList) { + if (!organizationalUnit['awsKey']) { + organizationalUnitsToCreate['push'](organizationalUnit); + } + } + } + + //get organzational root id + const rootId = await getRootId(); + console.log(`Root OU ID ${rootId}`); + + //sort by number of elements in order to + //create parent organizational units first + const sortedOrganizationalUnits = organizationalUnitsToCreate['sort']((a, b) => + a['acceleratorKey'].split('/').length > b['acceleratorKey'].split('/').length ? 1 : -1, + ); + + console.log(`Sorted list of OU's to create ${sortedOrganizationalUnits}`); + for (const organizationalUnit of sortedOrganizationalUnits) { + console.log(`Creating organizational unit ${organizationalUnit['acceleratorKey']}`); + const createResponse = await createOrganizationalUnitFromPath( + rootId, + organizationalUnit['acceleratorKey'], + configTableName, + ); + if (!createResponse) { + return { + Status: 'FAILURE', + }; + } + } + return { + Status: 'SUCCESS', + }; + + case 'Delete': + // Do Nothing + return { + Status: 'SUCCESS', + }; + } +} + +async function lookupOrganizationalUnit(name: string, parentId: string): Promise { + let nextToken: string | undefined = undefined; + const page = await throttlingBackOff(() => + organizationsClient.listOrganizationalUnitsForParent({ ParentId: parentId, NextToken: nextToken }).promise(), + ); + for (const ou of page.OrganizationalUnits ?? []) { + if (ou.Name == name) { + return ou.Id!; + } + nextToken = page.NextToken; + } + while (nextToken); + return ''; +} + +async function getConfigFromTable(configTableName: string, commitId: string): Promise { + const params = { + TableName: configTableName, + KeyConditionExpression: 'dataType = :hkey', + ExpressionAttributeValues: { + ':hkey': 'organization', + ':commitId': commitId, + }, + FilterExpression: 'contains (commitId, :commitId)', + }; + const items: DDBItems = []; + const paginator = paginateQuery(paginationConfig, params); + for await (const page of paginator) { + if (page.Items) { + for (const item of page.Items) { + items.push(item); + } + } + } + return items; +} + +async function getRootId(): Promise { + // get root ou id + let rootId = ''; + let nextToken: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => organizationsClient.listRoots({ NextToken: nextToken }).promise()); + for (const item of page.Roots ?? []) { + if (item.Name === 'Root' && item.Id && item.Arn) { + rootId = item.Id; + } + } + nextToken = page.NextToken; + } while (nextToken); + return rootId; +} + +function getPath(name: string): string { + //get the parent path + const pathIndex = name.lastIndexOf('/'); + const path = name.slice(0, pathIndex + 1).slice(0, -1); + if (path === '') { + return '/'; + } + return '/' + path; +} + +function getOuName(name: string): string { + const result = name.split('/').pop(); + if (result === undefined) { + return name; + } + return result; +} + +async function createOrganizationalUnitFromPath( + rootId: string, + acceleratorKey: string, + configTableName: string, +): Promise { + let parentId = rootId; + + const path = getPath(acceleratorKey); + const name = getOuName(acceleratorKey); + //find parent for ou + for (const parent of path.split('/')) { + if (parent) { + const orgId = await lookupOrganizationalUnit(parent, parentId); + if (orgId !== '') { + console.log(`Found parent ou with id ${orgId}`); + parentId = orgId; + } else { + console.log(`Need to create ou ${parent} for parentId ${parentId} in the organizations config`); + return false; + } + } + } + + // Create the OU if not found + try { + const organizationsResponse = await throttlingBackOff(() => + organizationsClient + .createOrganizationalUnit({ + Name: name, + ParentId: parentId, + }) + .promise(), + ); + console.log(`Created OU with id: ${organizationsResponse.OrganizationalUnit?.Id}`); + const params: UpdateCommandInput = { + TableName: configTableName, + Key: { + dataType: 'organization', + acceleratorKey: acceleratorKey, + }, + UpdateExpression: 'set #attribute = :x', + ExpressionAttributeNames: { '#attribute': 'awsKey' }, + ExpressionAttributeValues: { ':x': organizationsResponse.OrganizationalUnit?.Id }, + }; + const updateConfigReponse = await throttlingBackOff(() => documentClient.send(new UpdateCommand(params))); + console.log(updateConfigReponse); + return true; + } catch (error) { + console.log(error); + return false; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-organizational-units/package.json b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-organizational-units/package.json new file mode 100644 index 000000000..30562172a --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-organizational-units/package.json @@ -0,0 +1,44 @@ +{ + "name": "@aws-accelerator/constructs-aws-organizations-create-organizational-units", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --outfile=./dist/index.js --platform=node --target=node14 --external:aws-sdk index.ts", + "test": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "esbuild": "0.14.2", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1001.0", + "@aws-sdk/client-dynamodb": "3.92.0", + "@aws-sdk/lib-dynamodb": "3.92.0" + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-organizational-units/tsconfig.json b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-organizational-units/tsconfig.json new file mode 100644 index 000000000..ebcb98a79 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-organizational-units/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-policy/index.ts b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-policy/index.ts new file mode 100644 index 000000000..befb7ea7f --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-policy/index.ts @@ -0,0 +1,156 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { throttlingBackOff } from '@aws-accelerator/utils'; +import * as AWS from 'aws-sdk'; +AWS.config.logger = console; + +/** + * create-policy - lambda handler + * + * @param event + * @returns + */ +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + PhysicalResourceId: string | undefined; + Status: string; + } + | undefined +> { + const bucket: string = event.ResourceProperties['bucket']; + const key: string = event.ResourceProperties['key']; + const name: string = event.ResourceProperties['name']; + const description = event.ResourceProperties['description'] || ''; + const type: string = event.ResourceProperties['type']; + const tags: AWS.Organizations.Tag[] = event.ResourceProperties['tags'] || []; + const partition: string = event.ResourceProperties['partition']; + const acceleratorPrefix: string = event.ResourceProperties['acceleratorPrefix']; + const managementAccountAccessRole: string = event.ResourceProperties['managementAccountAccessRole']; + + let organizationsClient: AWS.Organizations; + if (partition === 'aws-us-gov') { + organizationsClient = new AWS.Organizations({ region: 'us-gov-west-1' }); + } else { + organizationsClient = new AWS.Organizations({ region: 'us-east-1' }); + } + const s3Client = new AWS.S3({}); + + switch (event.RequestType) { + case 'Create': + case 'Update': + // + // Read in the policy content from the specified S3 location + // + const s3Object = await throttlingBackOff(() => s3Client.getObject({ Bucket: bucket, Key: key }).promise()); + const content = s3Object.Body!.toString(); + console.log(content); + + // Minify and update placeholder values + let policyContent: string = JSON.stringify(JSON.parse(content)); + policyContent = replaceDefaults({ + content: policyContent, + acceleratorPrefix: acceleratorPrefix, + managementAccountAccessRole: managementAccountAccessRole, + partition: partition, + additionalReplacements: {}, + }); + // + // Check if already exists, update and return the ID + // + let nextToken: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => + organizationsClient.listPolicies({ Filter: type, NextToken: nextToken }).promise(), + ); + for (const policy of page.Policies ?? []) { + if (policy.Name === name) { + console.log('Existing Policy found'); + + if (policy.AwsManaged) { + return { + PhysicalResourceId: policy.Id, + Status: 'SUCCESS', + }; + } + + const response = await throttlingBackOff(() => + organizationsClient + .updatePolicy({ Name: name, Content: policyContent, Description: description, PolicyId: policy.Id! }) + .promise(), + ); + + console.log(response.Policy?.PolicySummary?.Id); + + return { + PhysicalResourceId: response.Policy?.PolicySummary?.Id, + Status: 'SUCCESS', + }; + } + } + nextToken = page.NextToken; + } while (nextToken); + + // + // Create if not found + // + const response = await throttlingBackOff(() => + organizationsClient + .createPolicy({ Content: policyContent, Description: description, Name: name, Tags: tags, Type: type }) + .promise(), + ); + + console.log(response.Policy?.PolicySummary?.Id); + + return { + PhysicalResourceId: response.Policy?.PolicySummary?.Id, + Status: 'SUCCESS', + }; + + case 'Delete': + // Do Nothing + return { + PhysicalResourceId: event.PhysicalResourceId, + Status: 'SUCCESS', + }; + } +} + +function replaceDefaults(props: { + content: string; + acceleratorPrefix: string; + managementAccountAccessRole: string; + partition: string; + additionalReplacements: { [key: string]: string | string[] }; +}): string { + const { acceleratorPrefix, additionalReplacements, managementAccountAccessRole, partition } = props; + let { content } = props; + + for (const [key, value] of Object.entries(additionalReplacements)) { + console.log(`key: ${key}, value: ${value}`); + content = content.replace(new RegExp(key, 'g'), StringType.is(value) ? value : JSON.stringify(value)); + } + + const replacements = { + '\\${MANAGEMENT_ACCOUNT_ACCESS_ROLE}': managementAccountAccessRole, + '\\${ACCELERATOR_PREFIX}': acceleratorPrefix, + '\\${PARTITION}': partition, + }; + + Object.entries(replacements).map(([key, value]) => { + content = content.replace(new RegExp(key, 'g'), value); + }); + console.log(`Policy with placeholder values ${content}`); + + return content; +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-policy/package.json b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-policy/package.json new file mode 100644 index 000000000..2262e5a8c --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-policy/package.json @@ -0,0 +1,44 @@ +{ + "name": "@aws-accelerator/constructs-aws-organizations-create-policy", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --outfile=./dist/index.js --platform=node --target=node14 --external:aws-sdk index.ts", + "test": "", + "testreport": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "esbuild": "0.14.2", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2", + "ts-node": "10.7.0" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1001.0" + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-policy/tsconfig.json b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-policy/tsconfig.json new file mode 100644 index 000000000..ebcb98a79 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/create-policy/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-organizations/describe-organization/index.ts b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/describe-organization/index.ts new file mode 100644 index 000000000..a33aec9ec --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/describe-organization/index.ts @@ -0,0 +1,56 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { throttlingBackOff } from '@aws-accelerator/utils'; +import * as AWS from 'aws-sdk'; +AWS.config.logger = console; + +/** + * describe-organization - lambda handler + * + * @param event + * @returns + */ +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + PhysicalResourceId: string | undefined; + Status: string; + } + | undefined +> { + switch (event.RequestType) { + case 'Create': + case 'Update': + const partition = event.ResourceProperties['partition']; + + let organizationsClient: AWS.Organizations; + if (partition === 'aws-us-gov') { + organizationsClient = new AWS.Organizations({ region: 'us-gov-west-1' }); + } else { + organizationsClient = new AWS.Organizations({ region: 'us-east-1' }); + } + + const response = await throttlingBackOff(() => organizationsClient.describeOrganization().promise()); + return { + PhysicalResourceId: response.Organization?.Id, + Status: 'SUCCESS', + }; + + case 'Delete': + // Do Nothing + return { + PhysicalResourceId: event.PhysicalResourceId, + Status: 'SUCCESS', + }; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-organizations/describe-organization/package.json b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/describe-organization/package.json new file mode 100644 index 000000000..23abba4b9 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/describe-organization/package.json @@ -0,0 +1,43 @@ +{ + "name": "@aws-accelerator/constructs-aws-organizations-describe-organization", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --outfile=./dist/index.js --platform=node --target=node14 --external:aws-sdk index.ts", + "test": "", + "testreport": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "esbuild": "0.14.2", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1001.0" + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-organizations/describe-organization/tsconfig.json b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/describe-organization/tsconfig.json new file mode 100644 index 000000000..ebcb98a79 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/describe-organization/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-organizations/enable-aws-service-access.ts b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/enable-aws-service-access.ts new file mode 100644 index 000000000..d8222d4f6 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/enable-aws-service-access.ts @@ -0,0 +1,85 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +const path = require('path'); + +/** + * EnableAwsServiceAccessProps properties + */ +export interface EnableAwsServiceAccessProps { + readonly servicePrincipal: string; + /** + * Custom resource lambda log group encryption key + */ + readonly kmsKey: cdk.aws_kms.Key; + /** + * Custom resource lambda log retention in days + */ + readonly logRetentionInDays: number; +} + +/** + * Class to Enable AWS Service Access + */ +export class EnableAwsServiceAccess extends Construct { + public readonly id: string; + + constructor(scope: Construct, id: string, props: EnableAwsServiceAccessProps) { + super(scope, id); + + const provider = cdk.CustomResourceProvider.getOrCreateProvider( + this, + 'Custom::OrganizationsEnableAwsServiceAccess', + { + codeDirectory: path.join(__dirname, 'enable-aws-service-access/dist'), + runtime: cdk.CustomResourceProviderRuntime.NODEJS_14_X, + policyStatements: [ + { + Effect: 'Allow', + Action: ['organizations:DisableAWSServiceAccess', 'organizations:EnableAwsServiceAccess'], + Resource: '*', + }, + ], + }, + ); + + const resource = new cdk.CustomResource(this, 'Resource', { + resourceType: 'Custom::EnableAwsServiceAccess', + serviceToken: provider.serviceToken, + properties: { + partition: cdk.Aws.PARTITION, + servicePrincipal: props.servicePrincipal, + }, + }); + + /** + * Singleton pattern to define the log group for the singleton function + * in the stack + */ + const stack = cdk.Stack.of(scope); + const logGroup = + (stack.node.tryFindChild(`${provider.node.id}LogGroup`) as cdk.aws_logs.LogGroup) ?? + new cdk.aws_logs.LogGroup(stack, `${provider.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${(provider.node.findChild('Handler') as cdk.aws_lambda.CfnFunction).ref}`, + retention: props.logRetentionInDays, + encryptionKey: props.kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + resource.node.addDependency(logGroup); + + this.id = resource.ref; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-organizations/enable-aws-service-access/index.ts b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/enable-aws-service-access/index.ts new file mode 100644 index 000000000..68ee76c10 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/enable-aws-service-access/index.ts @@ -0,0 +1,62 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { throttlingBackOff } from '@aws-accelerator/utils'; +import * as AWS from 'aws-sdk'; +AWS.config.logger = console; + +/** + * enable-aws-service-access - lambda handler + * + * @param event + * @returns + */ +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + PhysicalResourceId: string | undefined; + Status: string; + } + | undefined +> { + const servicePrincipal: string = event.ResourceProperties['servicePrincipal']; + const partition = event.ResourceProperties['partition']; + + let organizationsClient: AWS.Organizations; + if (partition === 'aws-us-gov') { + organizationsClient = new AWS.Organizations({ region: 'us-gov-west-1' }); + } else { + organizationsClient = new AWS.Organizations({ region: 'us-east-1' }); + } + + switch (event.RequestType) { + case 'Create': + case 'Update': + await throttlingBackOff(() => + organizationsClient.enableAWSServiceAccess({ ServicePrincipal: servicePrincipal }).promise(), + ); + + return { + PhysicalResourceId: servicePrincipal, + Status: 'SUCCESS', + }; + + case 'Delete': + await throttlingBackOff(() => + organizationsClient.disableAWSServiceAccess({ ServicePrincipal: servicePrincipal }).promise(), + ); + return { + PhysicalResourceId: event.PhysicalResourceId, + Status: 'SUCCESS', + }; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-organizations/enable-aws-service-access/package.json b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/enable-aws-service-access/package.json new file mode 100644 index 000000000..ee4fbcae8 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/enable-aws-service-access/package.json @@ -0,0 +1,43 @@ +{ + "name": "@aws-accelerator/constructs-aws-organizations-enable-aws-service-access", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --outfile=./dist/index.js --platform=node --target=node14 --external:aws-sdk index.ts", + "test": "", + "testreport": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "esbuild": "0.14.2", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1001.0" + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-organizations/enable-aws-service-access/tsconfig.json b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/enable-aws-service-access/tsconfig.json new file mode 100644 index 000000000..ebcb98a79 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/enable-aws-service-access/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-organizations/enable-policy-type.ts b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/enable-policy-type.ts new file mode 100644 index 000000000..4cdf393fc --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/enable-policy-type.ts @@ -0,0 +1,115 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +const path = require('path'); + +export enum PolicyTypeEnum { + SERVICE_CONTROL_POLICY = 'SERVICE_CONTROL_POLICY', + TAG_POLICY = 'TAG_POLICY', + BACKUP_POLICY = 'BACKUP_POLICY', + AISERVICES_OPT_OUT_POLICY = 'AISERVICES_OPT_OUT_POLICY', +} + +/** + * Initialized EnablePolicyType properties + */ +export interface EnablePolicyTypeProps { + readonly policyType: PolicyTypeEnum; + /** + * Custom resource lambda log group encryption key + */ + readonly kmsKey: cdk.aws_kms.Key; + /** + * Custom resource lambda log retention in days + */ + readonly logRetentionInDays: number; +} + +/** + * Class to initialize EnablePolicyType + */ +export class EnablePolicyType extends cdk.Resource { + constructor(scope: Construct, id: string, props: EnablePolicyTypeProps) { + super(scope, id); + + const ENABLE_POLICY_TYPE = 'Custom::EnablePolicyType'; + + const provider = cdk.CustomResourceProvider.getOrCreateProvider(this, ENABLE_POLICY_TYPE, { + codeDirectory: path.join(__dirname, 'enable-policy-type/dist'), + runtime: cdk.CustomResourceProviderRuntime.NODEJS_14_X, + policyStatements: [ + { + Effect: 'Allow', + Action: [ + 'organizations:DescribeOrganization', + 'organizations:DisablePolicyType', + 'organizations:EnablePolicyType', + 'organizations:ListRoots', + 'organizations:ListPoliciesForTarget', + 'organizations:ListTargetsForPolicy', + 'organizations:DescribeEffectivePolicy', + 'organizations:DescribePolicy', + 'organizations:DisableAWSServiceAccess', + 'organizations:DetachPolicy', + 'organizations:DeletePolicy', + 'organizations:DescribeAccount', + 'organizations:ListAWSServiceAccessForOrganization', + 'organizations:ListPolicies', + 'organizations:ListAccountsForParent', + 'organizations:ListAccounts', + 'organizations:EnableAWSServiceAccess', + 'organizations:ListCreateAccountStatus', + 'organizations:UpdatePolicy', + 'organizations:DescribeOrganizationalUnit', + 'organizations:AttachPolicy', + 'organizations:ListParents', + 'organizations:ListOrganizationalUnitsForParent', + 'organizations:CreatePolicy', + 'organizations:DescribeCreateAccountStatus', + ], + Resource: '*', + }, + ], + }); + + const resource = new cdk.CustomResource(this, 'Resource', { + resourceType: ENABLE_POLICY_TYPE, + serviceToken: provider.serviceToken, + properties: { + partition: cdk.Aws.PARTITION, + policyType: props.policyType, + }, + }); + + /** + * Singleton pattern to define the log group for the singleton function + * in the stack + */ + const stack = cdk.Stack.of(scope); + const logGroup = + (stack.node.tryFindChild(`${provider.node.id}LogGroup`) as cdk.aws_logs.LogGroup) ?? + new cdk.aws_logs.LogGroup(stack, `${provider.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${(provider.node.findChild('Handler') as cdk.aws_lambda.CfnFunction).ref}`, + retention: props.logRetentionInDays, + encryptionKey: props.kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + resource.node.addDependency(logGroup); + + // this.organizationalUnitId = resource.ref; + // this.organizationalUnitArn = resource.getAttString('arn'); + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-organizations/enable-policy-type/index.ts b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/enable-policy-type/index.ts new file mode 100644 index 000000000..f04e6200f --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/enable-policy-type/index.ts @@ -0,0 +1,88 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { throttlingBackOff } from '@aws-accelerator/utils'; +import * as AWS from 'aws-sdk'; +AWS.config.logger = console; + +/** + * enable-policy-type - lambda handler + * + * @param event + * @returns + */ +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + PhysicalResourceId: string | undefined; + Status: string; + } + | undefined +> { + switch (event.RequestType) { + case 'Create': + const policyType = event.ResourceProperties['policyType']; + const partition = event.ResourceProperties['partition']; + + // + // Obtain an Organizations client + // + let organizationsClient: AWS.Organizations; + if (partition === 'aws-us-gov') { + organizationsClient = new AWS.Organizations({ region: 'us-gov-west-1' }); + } else { + organizationsClient = new AWS.Organizations({ region: 'us-east-1' }); + } + + // Verify policy type from the listRoots call + let nextToken: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => organizationsClient.listRoots({ NextToken: nextToken }).promise()); + for (const item of page.Roots ?? []) { + if ( + partition === 'aws-us-gov' && + item.PolicyTypes?.find(item => item.Type === 'TAG_POLICY' || 'BACKUP_POLICY') + ) { + throw new Error(`Policy Type ${policyType} not supported.`); + } + if (item.Name === 'Root') { + if (item.PolicyTypes?.find(item => item.Type === policyType && item.Status === 'ENABLED')) { + return { + PhysicalResourceId: policyType, + Status: 'SUCCESS', + }; + } + + await throttlingBackOff(() => + organizationsClient.enablePolicyType({ PolicyType: policyType, RootId: item.Id! }).promise(), + ); + + return { + PhysicalResourceId: policyType, + Status: 'SUCCESS', + }; + } + } + nextToken = page.NextToken; + } while (nextToken); + + throw new Error(`Error enabling policy type for Root`); + + case 'Update': + case 'Delete': + // Do Nothing, leave Policy Type enabled + return { + PhysicalResourceId: event.PhysicalResourceId, + Status: 'SUCCESS', + }; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-organizations/enable-policy-type/package.json b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/enable-policy-type/package.json new file mode 100644 index 000000000..0e85539e4 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/enable-policy-type/package.json @@ -0,0 +1,43 @@ +{ + "name": "@aws-accelerator/constructs-aws-organizations-enable-policy-type", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --outfile=./dist/index.js --platform=node --target=node14 --external:aws-sdk index.ts", + "test": "", + "testreport": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "esbuild": "0.14.2", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1001.0" + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-organizations/enable-policy-type/tsconfig.json b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/enable-policy-type/tsconfig.json new file mode 100644 index 000000000..ebcb98a79 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/enable-policy-type/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-organizations/invite-account-to-organization/index.ts b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/invite-account-to-organization/index.ts new file mode 100644 index 000000000..f4dd77877 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/invite-account-to-organization/index.ts @@ -0,0 +1,117 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { throttlingBackOff } from '@aws-accelerator/utils'; +import * as AWS from 'aws-sdk'; +AWS.config.logger = console; + +/** + * invite-account-to-organization - lambda handler + * + * @param event + * @returns + */ +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + PhysicalResourceId: string | undefined; + Status: string; + } + | undefined +> { + switch (event.RequestType) { + case 'Create': + case 'Update': + const accountId = event.ResourceProperties['accountId']; + const roleArn = event.ResourceProperties['roleArn']; + const partition = event.ResourceProperties['partition']; + + // + // Obtain an Organizations client + // + let organizationsClient: AWS.Organizations; + if (partition === 'aws-us-gov') { + organizationsClient = new AWS.Organizations({ region: 'us-gov-west-1' }); + } else { + organizationsClient = new AWS.Organizations({ region: 'us-east-1' }); + } + + let nextToken: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => + organizationsClient.listAccounts({ NextToken: nextToken }).promise(), + ); + for (const item of page.Accounts ?? []) { + if (item.Id === accountId) { + console.log(`Account ${accountId} already added to organization`); + if (item.Status) + return { + PhysicalResourceId: accountId, + Status: 'SUCCESS', + }; + } + } + nextToken = page.NextToken; + } while (nextToken); + + // Account was not found, invite it + console.log('InviteAccountToOrganizationCommand'); + const invite = await throttlingBackOff(() => + organizationsClient.inviteAccountToOrganization({ Target: { Type: 'ACCOUNT', Id: accountId } }).promise(), + ); + console.log(invite); + console.log(`Invite handshake id: ${invite.Handshake?.Id}`); + + const stsClient = new AWS.STS({}); + + const assumeRoleResponse = await throttlingBackOff(() => + stsClient.assumeRole({ RoleArn: roleArn, RoleSessionName: 'AcceptHandshakeSession' }).promise(), + ); + + if (partition === 'aws-us-gov') { + organizationsClient = new AWS.Organizations({ + credentials: { + accessKeyId: assumeRoleResponse.Credentials?.AccessKeyId ?? '', + secretAccessKey: assumeRoleResponse.Credentials?.SecretAccessKey ?? '', + sessionToken: assumeRoleResponse.Credentials?.SessionToken, + }, + }); + } else { + organizationsClient = new AWS.Organizations({ + credentials: { + accessKeyId: assumeRoleResponse.Credentials?.AccessKeyId ?? '', + secretAccessKey: assumeRoleResponse.Credentials?.SecretAccessKey ?? '', + sessionToken: assumeRoleResponse.Credentials?.SessionToken, + }, + region: 'us-east-1', + }); + } + + console.log('AcceptHandshakeCommand'); + const response = await throttlingBackOff(() => + organizationsClient.acceptHandshake({ HandshakeId: invite.Handshake!.Id! }).promise(), + ); + console.log(response); + + return { + PhysicalResourceId: accountId, + Status: 'SUCCESS', + }; + + case 'Delete': + // Do Nothing, leave Policy Type enabled + return { + PhysicalResourceId: event.PhysicalResourceId, + Status: 'SUCCESS', + }; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-organizations/invite-account-to-organization/package.json b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/invite-account-to-organization/package.json new file mode 100644 index 000000000..e59fdacbc --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/invite-account-to-organization/package.json @@ -0,0 +1,43 @@ +{ + "name": "@aws-accelerator/constructs-aws-organizations-invite-account-to-organization", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --outfile=./dist/index.js --platform=node --target=node14 --external:aws-sdk index.ts", + "test": "", + "testreport": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "esbuild": "0.14.2", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1001.0" + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-organizations/invite-account-to-organization/tsconfig.json b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/invite-account-to-organization/tsconfig.json new file mode 100644 index 000000000..ebcb98a79 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/invite-account-to-organization/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-organizations/organization.ts b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/organization.ts new file mode 100644 index 000000000..ef45498f6 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/organization.ts @@ -0,0 +1,50 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +const path = require('path'); + +/** + * Class to initialize Organization + */ +export class Organization extends Construct { + public readonly id: string; + + public constructor(scope: Construct, id: string) { + super(scope, id); + + const provider = cdk.CustomResourceProvider.getOrCreateProvider(this, 'Custom::OrganizationsDescribeOrganization', { + codeDirectory: path.join(__dirname, 'describe-organization/dist'), + runtime: cdk.CustomResourceProviderRuntime.NODEJS_14_X, + policyStatements: [ + { + Effect: 'Allow', + Action: ['organizations:DescribeOrganization'], + Resource: '*', + }, + ], + }); + + const resource = new cdk.CustomResource(this, 'Resource', { + resourceType: 'Custom::DescribeOrganization', + serviceToken: provider.serviceToken, + properties: { + partition: cdk.Aws.PARTITION, + }, + }); + + this.id = resource.ref; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-organizations/organizational-units.ts b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/organizational-units.ts new file mode 100644 index 000000000..0aa03ac63 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/organizational-units.ts @@ -0,0 +1,105 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import { v4 as uuidv4 } from 'uuid'; + +const path = require('path'); + +/** + * Initialized OrganizationalUnit properties + */ +export interface OrganizationalUnitsProps { + readonly acceleratorConfigTable: cdk.aws_dynamodb.Table; + readonly commitId: string; + readonly controlTowerEnabled: boolean; + readonly organizationsEnabled: boolean; + /** + * Custom resource lambda log group encryption key + */ + readonly kmsKey: cdk.aws_kms.Key; + /** + * Custom resource lambda log retention in days + */ + readonly logRetentionInDays: number; +} + +/** + * Class to initialize OrganizationalUnits + */ +export class OrganizationalUnits extends Construct { + public readonly id: string; + + constructor(scope: Construct, id: string, props: OrganizationalUnitsProps) { + super(scope, id); + + const provider = cdk.CustomResourceProvider.getOrCreateProvider( + this, + 'Custom::OrganizationsCreateOrganizationalUnits', + { + codeDirectory: path.join(__dirname, 'create-organizational-units/dist'), + runtime: cdk.CustomResourceProviderRuntime.NODEJS_14_X, + policyStatements: [ + { + Sid: 'organizations', + Effect: 'Allow', + Action: [ + 'organizations:CreateOrganizationalUnit', + 'organizations:ListOrganizationalUnitsForParent', + 'organizations:ListRoots', + 'organizations:UpdateOrganizationalUnit', + ], + Resource: '*', + }, + { + Sid: 'dynamodb', + Effect: 'Allow', + Action: ['dynamodb:UpdateItem', 'dynamodb:Query'], + Resource: [props.acceleratorConfigTable.tableArn], + }, + ], + }, + ); + + const resource = new cdk.CustomResource(this, 'Resource', { + resourceType: 'Custom::CreateOrganizationalUnits', + serviceToken: provider.serviceToken, + properties: { + configTableName: props.acceleratorConfigTable.tableName, + commitId: props.commitId, + controlTowerEnabled: props.controlTowerEnabled, + organizationsEnabled: props.organizationsEnabled, + partition: cdk.Aws.PARTITION, + uuid: uuidv4(), + }, + }); + + /** + * Singleton pattern to define the log group for the singleton function + * in the stack + */ + const stack = cdk.Stack.of(scope); + const logGroup = + (stack.node.tryFindChild(`${provider.node.id}LogGroup`) as cdk.aws_logs.LogGroup) ?? + new cdk.aws_logs.LogGroup(stack, `${provider.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${(provider.node.findChild('Handler') as cdk.aws_lambda.CfnFunction).ref}`, + retention: props.logRetentionInDays, + encryptionKey: props.kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + resource.node.addDependency(logGroup); + + this.id = resource.ref; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-organizations/policy-attachment.ts b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/policy-attachment.ts new file mode 100644 index 000000000..b31708866 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/policy-attachment.ts @@ -0,0 +1,103 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { v4 as uuidv4 } from 'uuid'; +import { PolicyType } from './policy'; +import { Construct } from 'constructs'; + +const path = require('path'); + +/** + * Initialized Policy properties + */ +export interface PolicyAttachmentProps { + readonly policyId: string; + readonly targetId?: string; + readonly type: PolicyType; + /** + * Custom resource lambda log group encryption key + */ + readonly kmsKey: cdk.aws_kms.Key; + /** + * Custom resource lambda log retention in days + */ + readonly logRetentionInDays: number; +} + +/** + * Class to attach a Policy to an Organization Unit or Account + */ +export class PolicyAttachment extends Construct { + public readonly id: string; + public readonly policyId: string; + public readonly targetId: string | undefined; + public readonly type: PolicyType; + + constructor(scope: Construct, id: string, props: PolicyAttachmentProps) { + super(scope, id); + + this.policyId = props.policyId; + this.targetId = props.targetId; + this.type = props.type; + + // + // Function definition for the custom resource + // + const provider = cdk.CustomResourceProvider.getOrCreateProvider(this, 'Custom::OrganizationsAttachPolicy', { + codeDirectory: path.join(__dirname, 'attach-policy/dist'), + runtime: cdk.CustomResourceProviderRuntime.NODEJS_14_X, + policyStatements: [ + { + Effect: 'Allow', + Action: ['organizations:AttachPolicy', 'organizations:DetachPolicy', 'organizations:ListPoliciesForTarget'], + Resource: '*', + }, + ], + }); + + // + // Custom Resource definition. We want this resource to be evaluated on + // every CloudFormation update, so we generate a new uuid to force + // re-evaluation. + // + const resource = new cdk.CustomResource(this, 'Resource', { + resourceType: 'Custom::AttachPolicy', + serviceToken: provider.serviceToken, + properties: { + partition: cdk.Aws.PARTITION, + uuid: uuidv4(), + policyId: props.policyId, + targetId: props.targetId, + type: props.type, + }, + }); + + /** + * Singleton pattern to define the log group for the singleton function + * in the stack + */ + const stack = cdk.Stack.of(scope); + const logGroup = + (stack.node.tryFindChild(`${provider.node.id}LogGroup`) as cdk.aws_logs.LogGroup) ?? + new cdk.aws_logs.LogGroup(stack, `${provider.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${(provider.node.findChild('Handler') as cdk.aws_lambda.CfnFunction).ref}`, + retention: props.logRetentionInDays, + encryptionKey: props.kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + resource.node.addDependency(logGroup); + + this.id = resource.ref; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-organizations/policy.ts b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/policy.ts new file mode 100644 index 000000000..90baccc1e --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/policy.ts @@ -0,0 +1,175 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as assets from 'aws-cdk-lib/aws-s3-assets'; +import * as cdk from 'aws-cdk-lib'; +import { v4 as uuidv4 } from 'uuid'; +import { Construct } from 'constructs'; + +const path = require('path'); + +export enum PolicyType { + AISERVICES_OPT_OUT_POLICY = 'AISERVICES_OPT_OUT_POLICY', + BACKUP_POLICY = 'BACKUP_POLICY', + SERVICE_CONTROL_POLICY = 'SERVICE_CONTROL_POLICY', + TAG_POLICY = 'TAG_POLICY', +} + +/** + *

A custom key-value pair associated with a resource within your organization.

+ *

You can attach tags to any of the following organization resources.

+ *
    + *
  • + *

    AWS account

    + *
  • + *
  • + *

    Organizational unit (OU)

    + *
  • + *
  • + *

    Organization root

    + *
  • + *
  • + *

    Policy

    + *
  • + *
+ */ +export interface Tag { + /** + *

The key identifier, or name, of the tag.

+ */ + Key: string | undefined; + + /** + *

The string value that's associated with the key of the tag. You can set the value of a + * tag to an empty string, but you can't set the value of a tag to null.

+ */ + Value: string | undefined; +} + +/** + * Initialized Policy properties + */ +export interface PolicyProps { + readonly path: string; + readonly name: string; + readonly description?: string; + readonly type: PolicyType; + readonly tags?: Tag[]; + /** + * Custom resource lambda log group encryption key + */ + readonly kmsKey: cdk.aws_kms.Key; + /** + * Custom resource lambda log retention in days + */ + readonly logRetentionInDays: number; + readonly acceleratorPrefix: string; + readonly managementAccountAccessRole: string; +} + +/** + * Class to initialize Policy + */ +export class Policy extends Construct { + public readonly id: string; + public readonly path: string; + public readonly name: string; + public readonly description?: string; + public readonly type: PolicyType; + public readonly tags?: Tag[]; + + constructor(scope: Construct, id: string, props: PolicyProps) { + super(scope, id); + + this.path = props.path; + this.name = props.name; + this.description = props.description || ''; + this.type = props.type; + this.tags = props.tags || []; + + // + // Bundle the policy file. This will be available as an asset in S3 + // + const asset = new assets.Asset(this, 'Policy', { + path: props.path, + }); + + // + // Function definition for the custom resource + // + const provider = cdk.CustomResourceProvider.getOrCreateProvider(this, 'Custom::OrganizationsCreatePolicy', { + codeDirectory: path.join(__dirname, 'create-policy/dist'), + runtime: cdk.CustomResourceProviderRuntime.NODEJS_14_X, + description: 'Organizations create policy', + policyStatements: [ + { + Effect: 'Allow', + Action: ['organizations:CreatePolicy', 'organizations:ListPolicies', 'organizations:UpdatePolicy'], + Resource: '*', + }, + { + Effect: 'Allow', + Action: ['s3:GetObject'], + Resource: cdk.Stack.of(this).formatArn({ + service: 's3', + region: '', + account: '', + resource: asset.s3BucketName, + arnFormat: cdk.ArnFormat.SLASH_RESOURCE_NAME, + resourceName: '*', + }), + }, + ], + }); + + // + // Custom Resource definition. We want this resource to be evaluated on + // every CloudFormation update, so we generate a new uuid to force + // re-evaluation. + // + const resource = new cdk.CustomResource(this, 'Resource', { + resourceType: 'Custom::CreatePolicy', + serviceToken: provider.serviceToken, + properties: { + bucket: asset.s3BucketName, + key: asset.s3ObjectKey, + partition: cdk.Aws.PARTITION, + acceleratorPrefix: props.acceleratorPrefix, + managementAccountAccessRole: props.managementAccountAccessRole, + uuid: uuidv4(), + path: props.path, + name: props.name, + description: props.description, + type: props.type, + tags: props.tags, + }, + }); + + /** + * Singleton pattern to define the log group for the singleton function + * in the stack + */ + const stack = cdk.Stack.of(scope); + const logGroup = + (stack.node.tryFindChild(`${provider.node.id}LogGroup`) as cdk.aws_logs.LogGroup) ?? + new cdk.aws_logs.LogGroup(stack, `${provider.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${(provider.node.findChild('Handler') as cdk.aws_lambda.CfnFunction).ref}`, + retention: props.logRetentionInDays, + encryptionKey: props.kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + resource.node.addDependency(logGroup); + + this.id = resource.ref; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-organizations/register-delegated-administrator.ts b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/register-delegated-administrator.ts new file mode 100644 index 000000000..3a2b6b5a3 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/register-delegated-administrator.ts @@ -0,0 +1,88 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +const path = require('path'); + +/** + * RegisterDelegatedAdministratorProps properties + */ +export interface RegisterDelegatedAdministratorProps { + readonly servicePrincipal: string; + readonly accountId: string; + /** + * Custom resource lambda log group encryption key + */ + readonly kmsKey: cdk.aws_kms.Key; + /** + * Custom resource lambda log retention in days + */ + readonly logRetentionInDays: number; +} + +/** + * Class to Register a Delegated Administrator + * + * NOTE: This construct should only be used if the native service does not have + * it's own API or method to establish a delegated administrator + */ +export class RegisterDelegatedAdministrator extends Construct { + public readonly id: string; + + constructor(scope: Construct, id: string, props: RegisterDelegatedAdministratorProps) { + super(scope, id); + + const RESOURCE_TYPE = 'Custom::OrganizationsRegisterDelegatedAdministrator'; + + const provider = cdk.CustomResourceProvider.getOrCreateProvider(this, RESOURCE_TYPE, { + codeDirectory: path.join(__dirname, 'register-delegated-administrator/dist'), + runtime: cdk.CustomResourceProviderRuntime.NODEJS_14_X, + policyStatements: [ + { + Effect: 'Allow', + Action: ['organizations:DeregisterDelegatedAdministrator', 'organizations:RegisterDelegatedAdministrator'], + Resource: '*', + }, + ], + }); + + const resource = new cdk.CustomResource(this, 'Resource', { + resourceType: RESOURCE_TYPE, + serviceToken: provider.serviceToken, + properties: { + partition: cdk.Aws.PARTITION, + servicePrincipal: props.servicePrincipal, + accountId: props.accountId, + }, + }); + + /** + * Singleton pattern to define the log group for the singleton function + * in the stack + */ + const stack = cdk.Stack.of(scope); + const logGroup = + (stack.node.tryFindChild(`${provider.node.id}LogGroup`) as cdk.aws_logs.LogGroup) ?? + new cdk.aws_logs.LogGroup(stack, `${provider.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${(provider.node.findChild('Handler') as cdk.aws_lambda.CfnFunction).ref}`, + retention: props.logRetentionInDays, + encryptionKey: props.kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + resource.node.addDependency(logGroup); + + this.id = resource.ref; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-organizations/register-delegated-administrator/index.ts b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/register-delegated-administrator/index.ts new file mode 100644 index 000000000..25b11d613 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/register-delegated-administrator/index.ts @@ -0,0 +1,83 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { throttlingBackOff } from '@aws-accelerator/utils'; +import * as AWS from 'aws-sdk'; +AWS.config.logger = console; + +/** + * register-delegated-administrator - lambda handler + * + * @param event + * @returns + */ +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + PhysicalResourceId: string | undefined; + Status: string; + } + | undefined +> { + const servicePrincipal: string = event.ResourceProperties['servicePrincipal']; + const accountId: string = event.ResourceProperties['accountId']; + const partition = event.ResourceProperties['partition']; + + let organizationsClient: AWS.Organizations; + if (partition === 'aws-us-gov') { + organizationsClient = new AWS.Organizations({ region: 'us-gov-west-1' }); + } else { + organizationsClient = new AWS.Organizations({ region: 'us-east-1' }); + } + + switch (event.RequestType) { + case 'Create': + case 'Update': + try { + await throttlingBackOff(() => + organizationsClient + .registerDelegatedAdministrator({ ServicePrincipal: servicePrincipal, AccountId: accountId }) + .promise(), + ); + } catch ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + e: any + ) { + if ( + // SDKv2 Error Structure + e.code === 'AccountAlreadyRegisteredException' || + // SDKv3 Error Structure + e.name === 'AccountAlreadyRegisteredException' + ) { + console.warn(e.name + ': ' + e.message); + return; + } + } + + return { + PhysicalResourceId: servicePrincipal, + Status: 'SUCCESS', + }; + + case 'Delete': + await throttlingBackOff(() => + organizationsClient + .deregisterDelegatedAdministrator({ ServicePrincipal: servicePrincipal, AccountId: accountId }) + .promise(), + ); + + return { + PhysicalResourceId: event.PhysicalResourceId, + Status: 'SUCCESS', + }; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-organizations/register-delegated-administrator/package.json b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/register-delegated-administrator/package.json new file mode 100644 index 000000000..a444acfee --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/register-delegated-administrator/package.json @@ -0,0 +1,43 @@ +{ + "name": "@aws-accelerator/constructs-aws-organizations-register-delegated-administrator", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --outfile=./dist/index.js --platform=node --target=node14 --external:aws-sdk index.ts", + "test": "", + "testreport": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "esbuild": "0.14.2", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1001.0" + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-organizations/register-delegated-administrator/tsconfig.json b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/register-delegated-administrator/tsconfig.json new file mode 100644 index 000000000..ebcb98a79 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-organizations/register-delegated-administrator/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ram/enable-sharing-with-aws-organization.ts b/source/packages/@aws-accelerator/constructs/lib/aws-ram/enable-sharing-with-aws-organization.ts new file mode 100644 index 000000000..e74fddb81 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ram/enable-sharing-with-aws-organization.ts @@ -0,0 +1,84 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +const path = require('path'); + +export interface EnableSharingWithAwsOrganizationProps { + /** + * Custom resource lambda log group encryption key + */ + readonly kmsKey: cdk.aws_kms.Key; + /** + * Custom resource lambda log retention in days + */ + readonly logRetentionInDays: number; +} + +/** + * Class to initialize Policy + */ +export class EnableSharingWithAwsOrganization extends Construct { + readonly id: string; + + constructor(scope: Construct, id: string, props: EnableSharingWithAwsOrganizationProps) { + super(scope, id); + + const ENABLE_SHARING_WITH_AWS_ORGANIZATION_TYPE = 'Custom::EnableSharingWithAwsOrganization'; + + // + // Function definition for the custom resource + // + const provider = cdk.CustomResourceProvider.getOrCreateProvider(this, ENABLE_SHARING_WITH_AWS_ORGANIZATION_TYPE, { + codeDirectory: path.join(__dirname, 'enable-sharing-with-aws-organization/dist'), + runtime: cdk.CustomResourceProviderRuntime.NODEJS_14_X, + policyStatements: [ + { + Effect: 'Allow', + Action: [ + 'ram:EnableSharingWithAwsOrganization', + 'iam:CreateServiceLinkedRole', + 'organizations:EnableAWSServiceAccess', + 'organizations:ListAWSServiceAccessForOrganization', + 'organizations:DescribeOrganization', + ], + Resource: '*', + }, + ], + }); + + const resource = new cdk.CustomResource(this, 'Resource', { + resourceType: ENABLE_SHARING_WITH_AWS_ORGANIZATION_TYPE, + serviceToken: provider.serviceToken, + }); + + /** + * Singleton pattern to define the log group for the singleton function + * in the stack + */ + const stack = cdk.Stack.of(scope); + const logGroup = + (stack.node.tryFindChild(`${provider.node.id}LogGroup`) as cdk.aws_logs.LogGroup) ?? + new cdk.aws_logs.LogGroup(stack, `${provider.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${(provider.node.findChild('Handler') as cdk.aws_lambda.CfnFunction).ref}`, + retention: props.logRetentionInDays, + encryptionKey: props.kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + resource.node.addDependency(logGroup); + + this.id = resource.ref; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ram/enable-sharing-with-aws-organization/index.ts b/source/packages/@aws-accelerator/constructs/lib/aws-ram/enable-sharing-with-aws-organization/index.ts new file mode 100644 index 000000000..961d9c69b --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ram/enable-sharing-with-aws-organization/index.ts @@ -0,0 +1,48 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { throttlingBackOff } from '@aws-accelerator/utils'; +import * as AWS from 'aws-sdk'; +AWS.config.logger = console; +/** + * enable-sharing-with-organization - lambda handler + * + * @param event + * @returns + */ +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + PhysicalResourceId: string | undefined; + Status: string; + } + | undefined +> { + const client = new AWS.RAM({}); + + switch (event.RequestType) { + case 'Create': + case 'Update': + await throttlingBackOff(() => client.enableSharingWithAwsOrganization({}).promise()); + return { + PhysicalResourceId: `ram-enable-sharing-with-aws-organization`, + Status: 'SUCCESS', + }; + + case 'Delete': + // Do Nothing + return { + PhysicalResourceId: event.PhysicalResourceId, + Status: 'SUCCESS', + }; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ram/enable-sharing-with-aws-organization/package.json b/source/packages/@aws-accelerator/constructs/lib/aws-ram/enable-sharing-with-aws-organization/package.json new file mode 100644 index 000000000..1bfe24c92 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ram/enable-sharing-with-aws-organization/package.json @@ -0,0 +1,43 @@ +{ + "name": "@aws-accelerator/constructs-aws-ram-enable-sharing-with-aws-organization", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --outfile=./dist/index.js --platform=node --target=node14 --external:aws-sdk index.ts", + "test": "", + "testreport": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "esbuild": "0.14.2", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1001.0" + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ram/enable-sharing-with-aws-organization/tsconfig.json b/source/packages/@aws-accelerator/constructs/lib/aws-ram/enable-sharing-with-aws-organization/tsconfig.json new file mode 100644 index 000000000..ebcb98a79 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ram/enable-sharing-with-aws-organization/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ram/get-resource-share-item/index.ts b/source/packages/@aws-accelerator/constructs/lib/aws-ram/get-resource-share-item/index.ts new file mode 100644 index 000000000..96e9b9202 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ram/get-resource-share-item/index.ts @@ -0,0 +1,81 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { throttlingBackOff } from '@aws-accelerator/utils'; +import * as AWS from 'aws-sdk'; + +AWS.config.logger = console; + +/** + * get-resource-share-item - lambda handler + * + * @param event + * @returns + */ +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + PhysicalResourceId: string | undefined; + Data: { + arn: string; + }; + Status: string; + } + | { + PhysicalResourceId: string | undefined; + Status: string; + } + | undefined +> { + const ramClient = new AWS.RAM({}); + + switch (event.RequestType) { + case 'Create': + case 'Update': + const resourceOwner = event.ResourceProperties['resourceOwner']; + const resourceShareArn = event.ResourceProperties['resourceShareArn']; + const resourceType = event.ResourceProperties['resourceType']; + + let nextToken: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => + ramClient + .listResources({ resourceShareArns: [resourceShareArn], resourceType, resourceOwner, nextToken }) + .promise(), + ); + // Return the first item found with the specified filters + if (page.resources && page.resources.length > 0) { + const item = page.resources[0]; + if (item.arn) { + console.log(item.arn); + return { + PhysicalResourceId: item.arn.split('/')[1], + Data: { + arn: item.arn, + }, + Status: 'SUCCESS', + }; + } + } + nextToken = page.nextToken; + } while (nextToken); + + throw new Error(`Resource share item not found`); + + case 'Delete': + // Do Nothing + return { + PhysicalResourceId: event.PhysicalResourceId, + Status: 'SUCCESS', + }; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ram/get-resource-share-item/package.json b/source/packages/@aws-accelerator/constructs/lib/aws-ram/get-resource-share-item/package.json new file mode 100644 index 000000000..2bf52ee16 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ram/get-resource-share-item/package.json @@ -0,0 +1,43 @@ +{ + "name": "@aws-accelerator/constructs-aws-ram-get-resource-share-item", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --outfile=./dist/index.js --platform=node --target=node14 --external:aws-sdk index.ts", + "test": "", + "testreport": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "esbuild": "0.14.2", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1001.0" + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ram/get-resource-share-item/tsconfig.json b/source/packages/@aws-accelerator/constructs/lib/aws-ram/get-resource-share-item/tsconfig.json new file mode 100644 index 000000000..ebcb98a79 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ram/get-resource-share-item/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ram/get-resource-share/index.ts b/source/packages/@aws-accelerator/constructs/lib/aws-ram/get-resource-share/index.ts new file mode 100644 index 000000000..7418e1641 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ram/get-resource-share/index.ts @@ -0,0 +1,66 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { throttlingBackOff } from '@aws-accelerator/utils'; +import * as AWS from 'aws-sdk'; +AWS.config.logger = console; + +/** + * get-resource-share - lambda handler + * + * @param event + * @returns + */ +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + PhysicalResourceId: string | undefined; + Status: string; + } + | undefined +> { + const ramClient = new AWS.RAM({}); + + switch (event.RequestType) { + case 'Create': + case 'Update': + const resourceOwner = event.ResourceProperties['resourceOwner']; + const owningAccountId = event.ResourceProperties['owningAccountId']; + const name = event.ResourceProperties['name']; + + let nextToken: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => ramClient.getResourceShares({ resourceOwner, nextToken }).promise()); + for (const resourceShare of page.resourceShares ?? []) { + if (resourceShare.owningAccountId == owningAccountId && resourceShare.name === name) { + console.log(resourceShare); + if (resourceShare.resourceShareArn) { + return { + PhysicalResourceId: resourceShare.resourceShareArn.split('/')[1], + Status: 'SUCCESS', + }; + } + } + } + nextToken = page.nextToken; + } while (nextToken); + + throw new Error(`Resource share ${name} not found`); + + case 'Delete': + // Do Nothing + return { + PhysicalResourceId: event.PhysicalResourceId, + Status: 'SUCCESS', + }; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ram/get-resource-share/package.json b/source/packages/@aws-accelerator/constructs/lib/aws-ram/get-resource-share/package.json new file mode 100644 index 000000000..320fa5872 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ram/get-resource-share/package.json @@ -0,0 +1,43 @@ +{ + "name": "@aws-accelerator/constructs-aws-ram-get-resource-share", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --outfile=./dist/index.js --platform=node --target=node14 --external:aws-sdk index.ts", + "test": "", + "testreport": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "esbuild": "0.14.2", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1001.0" + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ram/get-resource-share/tsconfig.json b/source/packages/@aws-accelerator/constructs/lib/aws-ram/get-resource-share/tsconfig.json new file mode 100644 index 000000000..ebcb98a79 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ram/get-resource-share/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ram/resource-share.ts b/source/packages/@aws-accelerator/constructs/lib/aws-ram/resource-share.ts new file mode 100644 index 000000000..4e1a16bb8 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ram/resource-share.ts @@ -0,0 +1,232 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import * as ram from 'aws-cdk-lib/aws-ram'; +import { pascalCase } from 'change-case'; +import { Construct } from 'constructs'; +import { v4 as uuidv4 } from 'uuid'; + +const path = require('path'); + +export interface IResourceShare extends cdk.IResource { + /** + * The identifier of the resource share + * + * @attribute + */ + readonly resourceShareId: string; + + /** + * The name of the resource share + * + * @attribute + */ + readonly resourceShareName: string; + + /** + * The ARN of the resource share + * + * @attribute + */ + readonly resourceShareArn: string; + + /** + * The owner type of the resource share + * + * @attribute + */ + readonly resourceShareOwner: ResourceShareOwner; +} + +export enum ResourceShareOwner { + SELF = 'SELF', + OTHER_ACCOUNTS = 'OTHER-ACCOUNTS', +} + +export interface ResourceShareLookupOptions { + readonly resourceShareOwner: ResourceShareOwner; + readonly resourceShareName: string; + readonly owningAccountId?: string; +} + +export interface ResourceShareProps { + readonly name: string; + readonly allowExternalPrincipals?: boolean; + readonly permissionArns?: string[]; + readonly principals?: string[]; + readonly resourceArns?: string[]; +} + +export interface ResourceShareItemLookupOptions { + readonly resourceShare: IResourceShare; + readonly resourceShareItemType: string; + /** + * Custom resource lambda log group encryption key + */ + readonly kmsKey: cdk.aws_kms.Key; + /** + * Custom resource lambda log retention in days + */ + readonly logRetentionInDays: number; +} +export interface IResourceShareItem extends cdk.IResource { + readonly resourceShareItemArn: string; + + /** + * The identifier of the shared resource item + * + * @attribute + */ + readonly resourceShareItemId: string; +} + +export abstract class ResourceShareItem extends cdk.Resource implements IResourceShareItem { + readonly resourceShareItemArn: string = ''; + readonly resourceShareItemId: string = ''; + readonly resourceShareItemType: string = ''; + + public static fromLookup(scope: Construct, id: string, options: ResourceShareItemLookupOptions): IResourceShareItem { + class Import extends cdk.Resource implements IResourceShareItem { + public readonly resourceShareItemArn: string; + public readonly resourceShareItemId: string; + + constructor(scope: Construct, id: string) { + super(scope, id); + + console.log(options.resourceShare.resourceShareId); + const GET_RESOURCE_SHARE_ITEM = 'Custom::GetResourceShareItem'; + + const provider = cdk.CustomResourceProvider.getOrCreateProvider(this, GET_RESOURCE_SHARE_ITEM, { + codeDirectory: path.join(__dirname, 'get-resource-share-item/dist'), + runtime: cdk.CustomResourceProviderRuntime.NODEJS_14_X, + policyStatements: [ + { + Effect: 'Allow', + Action: ['ram:ListResources'], + Resource: '*', + }, + ], + }); + + const resource = new cdk.CustomResource(this, 'Resource', { + resourceType: GET_RESOURCE_SHARE_ITEM, + serviceToken: provider.serviceToken, + properties: { + resourceOwner: options.resourceShare.resourceShareOwner, + resourceShareArn: options.resourceShare.resourceShareArn, + resourceType: options.resourceShareItemType, + uuid: uuidv4(), // Generates a new UUID to force the resource to update + }, + }); + + /** + * Singleton pattern to define the log group for the singleton function + * in the stack + */ + const stack = cdk.Stack.of(scope); + const logGroup = + (stack.node.tryFindChild(`${provider.node.id}LogGroup`) as cdk.aws_logs.LogGroup) ?? + new cdk.aws_logs.LogGroup(stack, `${provider.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${(provider.node.findChild('Handler') as cdk.aws_lambda.CfnFunction).ref}`, + retention: options.logRetentionInDays, + encryptionKey: options.kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + resource.node.addDependency(logGroup); + + this.resourceShareItemId = resource.ref; + this.resourceShareItemArn = resource.getAtt('arn').toString(); + } + } + return new Import(scope, id); + } +} + +/** + * Creates a Resource Share + */ +export class ResourceShare extends cdk.Resource implements IResourceShare { + public static fromLookup(scope: Construct, id: string, options: ResourceShareLookupOptions): IResourceShare { + class Import extends cdk.Resource implements IResourceShare { + public readonly resourceShareId: string; + public readonly resourceShareName = options.resourceShareName; + public readonly resourceShareOwner = options.resourceShareOwner; + public readonly resourceShareArn; + + constructor(scope: Construct, id: string) { + super(scope, id); + const GET_RESOURCE_SHARE = 'Custom::GetResourceShare'; + + // + // Get the Resource Share Definition (by name) + // + const cr = cdk.CustomResourceProvider.getOrCreateProvider(this, GET_RESOURCE_SHARE, { + codeDirectory: path.join(__dirname, 'get-resource-share/dist'), + runtime: cdk.CustomResourceProviderRuntime.NODEJS_14_X, + policyStatements: [ + { + Effect: 'Allow', + Action: ['ram:GetResourceShares'], + Resource: '*', + }, + ], + }); + + const resource = new cdk.CustomResource(this, 'Resource', { + resourceType: GET_RESOURCE_SHARE, + serviceToken: cr.serviceToken, + properties: { + name: options.resourceShareName, + resourceOwner: options.resourceShareOwner, + owningAccountId: options.owningAccountId, + uuid: uuidv4(), // Generates a new UUID to force the resource to update + }, + }); + + this.resourceShareId = resource.ref; + + this.resourceShareArn = cdk.Stack.of(this).formatArn({ + service: 'ram', + account: options.owningAccountId, + resource: 'resource-share', + arnFormat: cdk.ArnFormat.SLASH_RESOURCE_NAME, + resourceName: this.resourceShareId, + }); + } + } + return new Import(scope, id); + } + + public readonly resourceShareId: string; + public readonly resourceShareName: string; + public readonly resourceShareArn: string; + public readonly resourceShareOwner: ResourceShareOwner; + + constructor(scope: Construct, id: string, props: ResourceShareProps) { + super(scope, id); + + const resource = new ram.CfnResourceShare(this, pascalCase(`${props.name}ResourceShare`), { + name: props.name, + allowExternalPrincipals: props.allowExternalPrincipals, + permissionArns: props.permissionArns, + principals: props.principals, + resourceArns: props.resourceArns, + }); + + this.resourceShareId = resource.ref; + this.resourceShareName = props.name; + this.resourceShareArn = resource.attrArn; + this.resourceShareOwner = ResourceShareOwner.SELF; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/endpoint-addresses.ts b/source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/endpoint-addresses.ts new file mode 100644 index 000000000..5b93c61e8 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/endpoint-addresses.ts @@ -0,0 +1,84 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import * as path from 'path'; + +export interface IEndpointAddresses extends cdk.IResource { + /** + * The IP addresses of the endpoint. + */ + readonly ipAddresses: cdk.Reference; +} + +export interface EndpointAddressesProps { + /** + * The ID of the Route 53 Resolver endpoint. + */ + readonly endpointId: string; + /** + * Custom resource lambda log group encryption key + */ + readonly kmsKey: cdk.aws_kms.Key; + /** + * Custom resource lambda log retention in days + */ + readonly logRetentionInDays: number; +} + +export class EndpointAddresses extends cdk.Resource implements IEndpointAddresses { + public ipAddresses: cdk.Reference; + + constructor(scope: Construct, id: string, props: EndpointAddressesProps) { + super(scope, id); + + const provider = cdk.CustomResourceProvider.getOrCreateProvider(this, 'Custom::ResolverEndpointAddresses', { + codeDirectory: path.join(__dirname, 'get-endpoint-addresses/dist'), + runtime: cdk.CustomResourceProviderRuntime.NODEJS_14_X, + policyStatements: [ + { + Effect: 'Allow', + Action: ['route53resolver:ListResolverEndpointIpAddresses'], + Resource: '*', + }, + ], + }); + + const resource = new cdk.CustomResource(this, 'Resource', { + resourceType: 'Custom::ResolverEndpointAddresses', + serviceToken: provider.serviceToken, + properties: { + endpointId: props.endpointId, + region: cdk.Stack.of(this).region, + }, + }); + + /** + * Singleton pattern to define the log group for the singleton function + * in the stack + */ + const stack = cdk.Stack.of(scope); + const logGroup = + (stack.node.tryFindChild(`${provider.node.id}LogGroup`) as cdk.aws_logs.LogGroup) ?? + new cdk.aws_logs.LogGroup(stack, `${provider.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${(provider.node.findChild('Handler') as cdk.aws_lambda.CfnFunction).ref}`, + retention: props.logRetentionInDays, + encryptionKey: props.kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + resource.node.addDependency(logGroup); + + this.ipAddresses = resource.getAtt('ipAddresses'); + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/firewall-domain-list.ts b/source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/firewall-domain-list.ts new file mode 100644 index 000000000..dc59f5e02 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/firewall-domain-list.ts @@ -0,0 +1,150 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import * as path from 'path'; + +export interface IResolverFirewallDomainList extends cdk.IResource { + /** + * The ID of the domain list. + */ + readonly listId: string; + + /** + * The name of the domain list. + */ + readonly name: string; + + /** + * The Amazon Resource Name (ARN) of the firewall domain list. + */ + readonly listArn?: string; +} + +export enum ResolverFirewallDomainListType { + CUSTOM = 'CUSTOM', + MANAGED = 'MANAGED', +} + +export interface ResolverFirewallDomainListProps { + /** + * The name of the domain list. + */ + readonly name: string; + + /** + * The type of the domain list. + */ + readonly type: ResolverFirewallDomainListType; + + /** + * Path to a file containing a domain list. + */ + readonly path?: string; + + /** + * A list of CloudFormation tags + */ + readonly tags?: cdk.CfnTag[]; + + /** + * Custom resource lambda log group encryption key + */ + readonly kmsKey: cdk.aws_kms.Key; + /** + * Custom resource lambda log retention in days + */ + readonly logRetentionInDays: number; +} + +export class ResolverFirewallDomainList extends cdk.Resource implements IResolverFirewallDomainList { + public readonly listId: string; + public readonly name: string; + public readonly listArn?: string; + private assetUrl?: string; + + constructor(scope: Construct, id: string, props: ResolverFirewallDomainListProps) { + super(scope, id); + + this.name = props.name; + + if (props.type === ResolverFirewallDomainListType.CUSTOM) { + // Check if path was provided + if (!props.path) { + throw new Error('path property must be specified when creating domain list of type CUSTOM'); + } + this.assetUrl = this.getAssetUrl(props.path); + + props.tags?.push({ key: 'Name', value: this.name }); + + // Create custom domain list with uploaded asset file + const resource = new cdk.aws_route53resolver.CfnFirewallDomainList(this, 'Resource', { + domainFileUrl: this.assetUrl, + tags: props.tags, + }); + + this.listArn = resource.attrArn; + this.listId = resource.attrId; + } else { + // Create custom resource provider + const RESOURCE_TYPE = 'Custom::ResolverManagedDomainList'; + + const provider = cdk.CustomResourceProvider.getOrCreateProvider(this, RESOURCE_TYPE, { + codeDirectory: path.join(__dirname, 'get-domain-lists/dist'), + runtime: cdk.CustomResourceProviderRuntime.NODEJS_14_X, + policyStatements: [ + { + Effect: 'Allow', + Action: ['route53resolver:ListFirewallDomainLists'], + Resource: '*', + }, + ], + }); + + // Get managed domain list ID + const resource = new cdk.CustomResource(this, 'Resource', { + resourceType: RESOURCE_TYPE, + serviceToken: provider.serviceToken, + properties: { + listName: props.name, + region: cdk.Stack.of(this).region, + }, + }); + + /** + * Singleton pattern to define the log group for the singleton function + * in the stack + */ + const stack = cdk.Stack.of(scope); + const logGroup = + (stack.node.tryFindChild(`${provider.node.id}LogGroup`) as cdk.aws_logs.LogGroup) ?? + new cdk.aws_logs.LogGroup(stack, `${provider.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${(provider.node.findChild('Handler') as cdk.aws_lambda.CfnFunction).ref}`, + retention: props.logRetentionInDays, + encryptionKey: props.kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + resource.node.addDependency(logGroup); + + this.listId = resource.ref; + } + } + + private getAssetUrl(path: string): string { + const asset = new cdk.aws_s3_assets.Asset(this, 'Asset', { + path: path, + }); + return asset.s3ObjectUrl; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/firewall-rule-group.ts b/source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/firewall-rule-group.ts new file mode 100644 index 000000000..0f7e8f7a5 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/firewall-rule-group.ts @@ -0,0 +1,112 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +export interface IResolverFirewallRuleGroup extends cdk.IResource { + /** + * The ARN (Amazon Resource Name) of the rule group. + */ + readonly groupArn: string; + + /** + * The ID of the rule group. + */ + readonly groupId: string; + + /** + * The name of the rule group. + */ + readonly name: string; +} + +export interface ResolverFirewallRuleGroupProps { + /** + * A list of the rules that you have defined. + */ + readonly firewallRules: cdk.aws_route53resolver.CfnFirewallRuleGroup.FirewallRuleProperty[]; + + /** + * The name of the rule group. + */ + readonly name: string; + + /** + * A list of CloudFormation tags. + */ + readonly tags?: cdk.CfnTag[]; +} + +export class ResolverFirewallRuleGroup extends cdk.Resource implements IResolverFirewallRuleGroup { + public readonly groupArn: string; + public readonly groupId: string; + public readonly name: string; + + constructor(scope: Construct, id: string, props: ResolverFirewallRuleGroupProps) { + super(scope, id); + + this.name = props.name; + props.tags?.push({ key: 'Name', value: this.name }); + + const resource = new cdk.aws_route53resolver.CfnFirewallRuleGroup(this, 'Resource', { + firewallRules: props.firewallRules, + tags: props.tags, + }); + + this.groupArn = resource.attrArn; + this.groupId = resource.ref; + } +} + +export interface ResolverFirewallRuleGroupAssociationProps { + /** + * The unique identifier of the firewall rule group. + */ + readonly firewallRuleGroupId: string; + + /** + * The setting that determines the processing order of the rule group + * among the rule groups that are associated with a single VPC. + */ + readonly priority: number; + + /** + * The unique identifier of the VPC that is associated with the rule group. + */ + readonly vpcId: string; + + /** + * If enabled, this setting disallows modification or removal of the association + */ + readonly mutationProtection?: string; + + /** + * A list of CloudFormation tags. + */ + readonly tags?: cdk.CfnTag[]; +} + +export class ResolverFirewallRuleGroupAssociation extends cdk.Resource { + constructor(scope: Construct, id: string, props: ResolverFirewallRuleGroupAssociationProps) { + super(scope, id); + + new cdk.aws_route53resolver.CfnFirewallRuleGroupAssociation(this, 'Resource', { + firewallRuleGroupId: props.firewallRuleGroupId, + priority: props.priority, + vpcId: props.vpcId, + mutationProtection: props.mutationProtection, + tags: props.tags, + }); + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/get-domain-lists/index.ts b/source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/get-domain-lists/index.ts new file mode 100644 index 000000000..d4c62bcd9 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/get-domain-lists/index.ts @@ -0,0 +1,72 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as AWS from 'aws-sdk'; + +import { throttlingBackOff } from '@aws-accelerator/utils'; + +AWS.config.logger = console; + +/** + * Get Route 53 resolver endpoint details - Lambda handler + * + * @param event + * @returns + */ +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + PhysicalResourceId: string; + Status: string; + } + | undefined +> { + const region: string = event.ResourceProperties['region']; + const listName: string = event.ResourceProperties['listName']; + const resolverClient = new AWS.Route53Resolver({ region: region }); + + switch (event.RequestType) { + case 'Create': + case 'Update': + let nextToken: string | undefined = undefined; + let resourceId: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => + resolverClient.listFirewallDomainLists({ NextToken: nextToken }).promise(), + ); + + // Loop through IP addresses and push to array + for (const item of page.FirewallDomainLists ?? []) { + if (item.Name === listName) { + resourceId = item.Id; + } + } + nextToken = page.NextToken; + } while (nextToken); + + if (!resourceId) { + throw new Error(`Managed domain list ${listName} does not exist.`); + } + + return { + PhysicalResourceId: resourceId, + Status: 'SUCCESS', + }; + + case 'Delete': + // Do nothing + return { + PhysicalResourceId: event.PhysicalResourceId, + Status: 'SUCCESS', + }; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/get-domain-lists/package.json b/source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/get-domain-lists/package.json new file mode 100644 index 000000000..cf3359256 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/get-domain-lists/package.json @@ -0,0 +1,44 @@ +{ + "name": "@aws-accelerator/constructs-aws-route53-resolver-get-domain-lists", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "description": "A custom resource to describe Route 53 Resolver endpoint details", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "private": true, + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --outfile=./dist/index.js --platform=node --target=node14 --external:aws-sdk index.ts", + "test": "", + "testreport": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "esbuild": "0.14.2", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1001.0" + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/get-domain-lists/tsconfig.json b/source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/get-domain-lists/tsconfig.json new file mode 100644 index 000000000..ebcb98a79 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/get-domain-lists/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/get-endpoint-addresses/index.ts b/source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/get-endpoint-addresses/index.ts new file mode 100644 index 000000000..3dd156a2f --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/get-endpoint-addresses/index.ts @@ -0,0 +1,81 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as AWS from 'aws-sdk'; + +import { throttlingBackOff } from '@aws-accelerator/utils'; + +AWS.config.logger = console; + +/** + * Get Route 53 resolver endpoint details - Lambda handler + * + * @param event + * @returns + */ +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + PhysicalResourceId: string; + Data: { + ipAddresses: { Ip: string }[]; + }; + Status: string; + } + | { + PhysicalResourceId: string; + Status: string; + } + | undefined +> { + const region: string = event.ResourceProperties['region']; + const endpointId: string = event.ResourceProperties['endpointId']; + const resolverClient = new AWS.Route53Resolver({ region: region }); + + switch (event.RequestType) { + case 'Create': + case 'Update': + const ipArray: { Ip: string }[] = []; + let nextToken: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => + resolverClient + .listResolverEndpointIpAddresses({ ResolverEndpointId: endpointId, NextToken: nextToken }) + .promise(), + ); + + // Loop through IP addresses and push to array + for (const item of page.IpAddresses ?? []) { + if (item.Ip) { + ipArray.push({ Ip: item.Ip }); + } + } + + nextToken = page.NextToken; + } while (nextToken); + + return { + PhysicalResourceId: endpointId, + Data: { + ipAddresses: ipArray, + }, + Status: 'SUCCESS', + }; + + case 'Delete': + // Do nothing + return { + PhysicalResourceId: event.PhysicalResourceId, + Status: 'SUCCESS', + }; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/get-endpoint-addresses/package.json b/source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/get-endpoint-addresses/package.json new file mode 100644 index 000000000..2f094dd60 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/get-endpoint-addresses/package.json @@ -0,0 +1,44 @@ +{ + "name": "@aws-accelerator/constructs-aws-route53-resolver-get-endpoint-addresses", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "description": "A custom resource to describe Route 53 Resolver endpoint details", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "private": true, + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --outfile=./dist/index.js --platform=node --target=node14 --external:aws-sdk index.ts", + "test": "", + "testreport": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "esbuild": "0.14.2", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1001.0" + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/get-endpoint-addresses/tsconfig.json b/source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/get-endpoint-addresses/tsconfig.json new file mode 100644 index 000000000..ebcb98a79 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/get-endpoint-addresses/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/query-logging-config.ts b/source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/query-logging-config.ts new file mode 100644 index 000000000..48e2602c4 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/query-logging-config.ts @@ -0,0 +1,121 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +export interface IQueryLoggingConfig extends cdk.IResource { + /** + * The Amazon Resource Name (ARN) for the query logging configuration. + */ + readonly logArn: string; + + /** + * The ID for the query logging configuration. + */ + readonly logId: string; + + /** + * The name that you assigned to the query logging config. + */ + readonly name: string; +} + +export interface QueryLoggingConfigProps { + /** + * The resource that you want Resolver to send query logs. + */ + readonly destination: cdk.aws_s3.IBucket | cdk.aws_logs.LogGroup; + + /** + * The name of the query logging configuration. + */ + readonly name: string; + + /** + * An AWS Organization ID, if the destination is CloudWatch Logs. + */ + readonly organizationId?: string; +} + +export class QueryLoggingConfig extends cdk.Resource implements IQueryLoggingConfig { + public readonly logArn: string; + public readonly logId: string; + public readonly name: string; + private destinationArn: string; + + constructor(scope: Construct, id: string, props: QueryLoggingConfigProps) { + super(scope, id); + + this.name = props.name; + + if (props.destination instanceof cdk.aws_logs.LogGroup) { + if (!props.organizationId) { + throw new Error('organizationId property must be defined when specifying a CloudWatch log group destination'); + } + this.addPermissions(props.destination, props.organizationId); + this.destinationArn = props.destination.logGroupArn; + } else if ('bucketName' in props.destination) { + this.destinationArn = props.destination.bucketArn; + } else { + throw new Error('Invalid resource type specified for destination property'); + } + + const resource = new cdk.aws_route53resolver.CfnResolverQueryLoggingConfig(this, 'Resource', { + destinationArn: this.destinationArn, + }); + + this.logArn = resource.attrArn; + this.logId = resource.attrId; + } + + private addPermissions(logGroup: cdk.aws_logs.LogGroup, orgId: string) { + logGroup.addToResourcePolicy( + new cdk.aws_iam.PolicyStatement({ + sid: 'Allow log delivery access', + effect: cdk.aws_iam.Effect.ALLOW, + principals: [new cdk.aws_iam.ServicePrincipal('delivery.logs.amazonaws.com')], + actions: ['logs:CreateLogStream', 'logs:PutLogEvents'], + resources: [`${logGroup.logGroupArn}:log-stream:*`], + conditions: { + StringEquals: { + 'aws:PrincipalOrgId': orgId, + }, + }, + }), + ); + } +} + +export interface QueryLoggingConfigAssociationProps { + /** + * The ID of the query logging configuration that a VPC is associated with. + */ + readonly resolverQueryLogConfigId?: string; + + /** + * The ID of the Amazon VPC that is associated with the query logging configuration. + */ + readonly vpcId?: string; +} + +export class QueryLoggingConfigAssociation extends cdk.Resource { + constructor(scope: Construct, id: string, props: QueryLoggingConfigAssociationProps) { + super(scope, id); + + new cdk.aws_route53resolver.CfnResolverQueryLoggingConfigAssociation(this, 'Resource', { + resolverQueryLogConfigId: props.resolverQueryLogConfigId, + resourceId: props.vpcId, + }); + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/resolver-endpoint.ts b/source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/resolver-endpoint.ts new file mode 100644 index 000000000..87ffdaf78 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/resolver-endpoint.ts @@ -0,0 +1,88 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +export interface IResolverEndpoint extends cdk.IResource { + /** + * The Amazon Resource Name (ARN) of the resolver endpoint. + */ + readonly endpointArn: string; + + /** + * The ID of the resolver endpoint. + */ + readonly endpointId: string; + + /** + * The name that you assigned to the resolver endpoint when you created the endpoint. + */ + readonly name: string; +} + +export interface ResolverEndpointProps { + /** + * Indicates whether the Resolver endpoint allows inbound or outbound DNS queries. + */ + readonly direction: string; + + /** + * The subnets and IP addresses in your VPC that DNS queries originate from (for outbound endpoints) + * or that you forward DNS queries to (for inbound endpoints). + */ + readonly ipAddresses: string[]; + + /** + * A friendly name that lets you easily find a configuration in the Resolver dashboard in the Route 53 console. + */ + readonly name: string; + + /** + * The ID of one or more security groups that control access to this endpoint. + */ + readonly securityGroupIds: string[]; + + /** + * A list of CloudFormation tags. + */ + readonly tags?: cdk.CfnTag[]; +} + +export class ResolverEndpoint extends cdk.Resource implements IResolverEndpoint { + public readonly endpointArn: string; + public readonly endpointId: string; + public readonly name: string; + private ipAddresses: cdk.aws_route53resolver.CfnResolverEndpoint.IpAddressRequestProperty[]; + + constructor(scope: Construct, id: string, props: ResolverEndpointProps) { + super(scope, id); + + this.ipAddresses = props.ipAddresses.map(item => { + return { subnetId: item }; + }); + + props.tags?.push({ key: 'Name', value: props.name }); + + const resource = new cdk.aws_route53resolver.CfnResolverEndpoint(this, 'Resource', { + direction: props.direction, + ipAddresses: this.ipAddresses, + securityGroupIds: props.securityGroupIds, + tags: props.tags, + }); + + this.endpointArn = resource.attrArn; + this.endpointId = resource.attrResolverEndpointId; + this.name = resource.attrName; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/resolver-rule.ts b/source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/resolver-rule.ts new file mode 100644 index 000000000..3a181b53a --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-route-53-resolver/resolver-rule.ts @@ -0,0 +1,145 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +import { EndpointAddresses } from './endpoint-addresses'; + +export interface IResolverRule extends cdk.IResource { + /** + * The friendly name of the resolver rule. + */ + readonly name: string; + + /** + * The Amazon Resource Name (ARN) of the resolver rule. + */ + readonly ruleArn: string; + + /** + * The ID that Resolver assigned to the resolver rule when you created it. + */ + readonly ruleId: string; +} + +export interface ResolverRuleProps { + /** + * DNS queries for this domain name are forwarded to the IP addresses that are specified in `TargetIps`. + */ + readonly domainName: string; + + /** + * The name for the Resolver rule. + */ + readonly name: string; + + /** + * The ID of the endpoint that the rule is associated with. + */ + readonly resolverEndpointId: string; + + /** + * The type of resolver rule: FORWARD, RECURSIVE, or SYSTEM. + * + * @default FORWARD + */ + readonly ruleType?: string; + + /** + * Choose to target an inbound resolver endpoint for name resolution. + */ + readonly targetInbound?: string; + + /** + * An array that contains the IP addresses and ports that an outbound endpoint forwards DNS queries to. + */ + readonly targetIps?: cdk.aws_route53resolver.CfnResolverRule.TargetAddressProperty[]; + + /** + * A list of CloudFormation tags. + */ + readonly tags?: cdk.CfnTag[]; + + /** + * Custom resource lambda log group encryption key + */ + readonly kmsKey: cdk.aws_kms.Key; + /** + * Custom resource lambda log retention in days + */ + readonly logRetentionInDays: number; +} + +export class ResolverRule extends cdk.Resource implements IResolverRule { + public readonly name: string; + public readonly ruleArn: string; + public readonly ruleId: string; + private targetIps?: cdk.aws_route53resolver.CfnResolverRule.TargetAddressProperty[] | cdk.Reference; + + constructor(scope: Construct, id: string, props: ResolverRuleProps) { + super(scope, id); + + this.name = props.name; + props.tags?.push({ key: 'Name', value: this.name }); + + if (props.targetInbound) { + this.targetIps = this.lookupInbound(props.targetInbound, props.kmsKey, props.logRetentionInDays); + } else { + this.targetIps = props.targetIps; + } + + const resource = new cdk.aws_route53resolver.CfnResolverRule(this, 'Resource', { + domainName: props.domainName, + resolverEndpointId: props.resolverEndpointId, + ruleType: props.ruleType ?? 'FORWARD', + targetIps: this.targetIps, + tags: props.tags, + }); + + this.ruleArn = resource.attrArn; + this.ruleId = resource.attrResolverRuleId; + } + + private lookupInbound(endpointId: string, kmsKey: cdk.aws_kms.Key, logRetentionInDays: number): cdk.Reference { + const lookup = new EndpointAddresses(this, 'LookupInbound', { + endpointId: endpointId, + kmsKey, + logRetentionInDays, + }); + return lookup.ipAddresses; + } +} + +export interface ResolverRuleAssociationProps { + /** + * The ID of the Resolver rule to associate. + */ + readonly resolverRuleId: string; + + /** + * The ID of the VPC to associate. + */ + readonly vpcId: string; +} + +export class ResolverRuleAssociation extends cdk.Resource { + constructor(scope: Construct, id: string, props: ResolverRuleAssociationProps) { + super(scope, id); + + new cdk.aws_route53resolver.CfnResolverRuleAssociation(this, 'Resource', { + resolverRuleId: props.resolverRuleId, + vpcId: props.vpcId, + }); + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-route-53/associate-hosted-zones.ts b/source/packages/@aws-accelerator/constructs/lib/aws-route-53/associate-hosted-zones.ts new file mode 100644 index 000000000..ae74c02fb --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-route-53/associate-hosted-zones.ts @@ -0,0 +1,96 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +const path = require('path'); + +export interface AssociateHostedZonesProps { + readonly accountIds: string[]; + readonly hostedZoneIds: string[]; + readonly hostedZoneAccountId: string; + readonly roleName: string; + readonly tagFilters: { + key: string; + value: string; + }[]; + /** + * Custom resource lambda log group encryption key + */ + readonly kmsKey: cdk.aws_kms.Key; + /** + * Custom resource lambda log retention in days + */ + readonly logRetentionInDays: number; +} + +export class AssociateHostedZones extends cdk.Resource { + public readonly id: string = ''; + + constructor(scope: Construct, id: string, props: AssociateHostedZonesProps) { + super(scope, id); + + const RESOURCE_TYPE = 'Custom::Route53AssociateHostedZones'; + + const provider = cdk.CustomResourceProvider.getOrCreateProvider(this, RESOURCE_TYPE, { + codeDirectory: path.join(__dirname, 'associate-hosted-zones/dist'), + runtime: cdk.CustomResourceProviderRuntime.NODEJS_14_X, + policyStatements: [ + { + Sid: 'Route53AssociateHostedZonesActions', + Effect: 'Allow', + Action: [ + 'ec2:DescribeVpcs', + 'route53:AssociateVPCWithHostedZone', + 'route53:CreateVPCAssociationAuthorization', + 'route53:DeleteVPCAssociationAuthorization', + 'route53:GetHostedZone', + 'sts:AssumeRole', + ], + Resource: '*', + }, + ], + }); + + const resource = new cdk.CustomResource(this, 'Resource', { + resourceType: RESOURCE_TYPE, + serviceToken: provider.serviceToken, + properties: { + partition: cdk.Stack.of(this).partition, + region: cdk.Stack.of(this).region, + accountIds: props.accountIds, + hostedZoneIds: props.hostedZoneIds, + hostedZoneAccountId: props.hostedZoneAccountId, + roleName: props.roleName, + tagFilters: props.tagFilters, + }, + }); + + /** + * Singleton pattern to define the log group for the singleton function + * in the stack + */ + const stack = cdk.Stack.of(scope); + const logGroup = + (stack.node.tryFindChild(`${provider.node.id}LogGroup`) as cdk.aws_logs.LogGroup) ?? + new cdk.aws_logs.LogGroup(stack, `${provider.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${(provider.node.findChild('Handler') as cdk.aws_lambda.CfnFunction).ref}`, + retention: props.logRetentionInDays, + encryptionKey: props.kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + resource.node.addDependency(logGroup); + this.id = resource.ref; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-route-53/associate-hosted-zones/index.ts b/source/packages/@aws-accelerator/constructs/lib/aws-route-53/associate-hosted-zones/index.ts new file mode 100644 index 000000000..875a2ea84 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-route-53/associate-hosted-zones/index.ts @@ -0,0 +1,164 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { throttlingBackOff } from '@aws-accelerator/utils'; +import * as AWS from 'aws-sdk'; +AWS.config.logger = console; + +/** + * associate-hosted-zones - lambda handler + * + * @param event + * @returns + */ +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + PhysicalResourceId: string | undefined; + Status: string; + } + | undefined +> { + switch (event.RequestType) { + case 'Create': + case 'Update': + const accountIds: string[] = event.ResourceProperties['accountIds']; + const hostedZoneIds: string[] = event.ResourceProperties['hostedZoneIds']; + const hostedZoneAccountId = event.ResourceProperties['hostedZoneAccountId']; + const partition = event.ResourceProperties['partition']; + const region = event.ResourceProperties['region']; + const roleName = event.ResourceProperties['roleName']; + const tagFilters: { + key: string; + value: string; + }[] = event.ResourceProperties['tagFilters']; + + // Loop through all the associated accounts + for (const accountId of accountIds ?? []) { + // + // Create clients + // + let targetEc2Client: AWS.EC2; + let targetRoute53Client: AWS.Route53; + const hostedZoneRoute53Client: AWS.Route53 = new AWS.Route53({}); + + if (accountId === hostedZoneAccountId) { + console.log('Running in hosted zone account, create local clients'); + targetEc2Client = new AWS.EC2({}); + targetRoute53Client = new AWS.Route53({}); + } else { + console.log('Not running in hosted zone account, assume role to create clients'); + const stsClient = new AWS.STS({}); + const assumeRoleResponse = await throttlingBackOff(() => + stsClient + .assumeRole({ + RoleArn: `arn:${partition}:iam::${accountId}:role/${roleName}`, + RoleSessionName: 'AssociateHostedZone', + }) + .promise(), + ); + + targetEc2Client = new AWS.EC2({ + credentials: { + accessKeyId: assumeRoleResponse.Credentials?.AccessKeyId ?? '', + secretAccessKey: assumeRoleResponse.Credentials?.SecretAccessKey ?? '', + sessionToken: assumeRoleResponse.Credentials?.SessionToken, + }, + }); + + targetRoute53Client = new AWS.Route53({ + credentials: { + accessKeyId: assumeRoleResponse.Credentials?.AccessKeyId ?? '', + secretAccessKey: assumeRoleResponse.Credentials?.SecretAccessKey ?? '', + sessionToken: assumeRoleResponse.Credentials?.SessionToken, + }, + }); + } + + // + // Find all the VPCs in the account to create associations + // + let nextToken: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => targetEc2Client.describeVpcs({ NextToken: nextToken }).promise()); + for (const vpc of page.Vpcs ?? []) { + console.log(`Checking vpc: ${vpc.VpcId}`); + console.log('Tags:'); + console.log(vpc.Tags); + + // Verify all tag filters are present for the VPC + let includeVpc = true; + tagFilters.forEach(tagFilter => { + if (!vpc.Tags?.find(tagItem => tagItem.Key === tagFilter.key && tagItem.Value == tagFilter.value)) { + includeVpc = false; + } + }); + + if (includeVpc) { + // Create the association for each hosted zone + for (const hostedZoneId of hostedZoneIds ?? []) { + // Check if vpc is already connected to the HostedZone + const response = await throttlingBackOff(() => + hostedZoneRoute53Client.getHostedZone({ Id: hostedZoneId }).promise(), + ); + if (response.VPCs?.find(item => item.VPCId === vpc.VpcId && item.VPCRegion === region)) { + console.log(`${vpc.VpcId} is already attached to the hosted zone ${hostedZoneId}`); + continue; + } + + const hostedZoneProps = { + HostedZoneId: hostedZoneId, + VPC: { + VPCId: vpc.VpcId, + VPCRegion: region, + }, + }; + + // authorize association of VPC with Hosted zones when VPC and Hosted Zones are defined in two different accounts + if (accountId !== hostedZoneAccountId) { + await throttlingBackOff(() => + hostedZoneRoute53Client.createVPCAssociationAuthorization(hostedZoneProps).promise(), + ); + } + + // associate VPC with Hosted zones + console.log(`Associating hosted zone ${hostedZoneId} with VPC ${vpc.VpcId}...`); + await throttlingBackOff(() => + targetRoute53Client.associateVPCWithHostedZone(hostedZoneProps).promise(), + ); + + // delete association of VPC with Hosted zones when VPC and Hosted Zones are defined in two different accounts + if (accountId !== hostedZoneAccountId) { + await throttlingBackOff(() => + hostedZoneRoute53Client.deleteVPCAssociationAuthorization(hostedZoneProps).promise(), + ); + } + } + } + } + nextToken = page.NextToken; + } while (nextToken); + } + + return { + PhysicalResourceId: 'associate-hosted-zones', + Status: 'SUCCESS', + }; + + case 'Delete': + // Do Nothing + return { + PhysicalResourceId: event.PhysicalResourceId, + Status: 'SUCCESS', + }; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-route-53/associate-hosted-zones/package.json b/source/packages/@aws-accelerator/constructs/lib/aws-route-53/associate-hosted-zones/package.json new file mode 100644 index 000000000..bf832a729 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-route-53/associate-hosted-zones/package.json @@ -0,0 +1,43 @@ +{ + "name": "@aws-accelerator/constructs-aws-route53-associate-hosted-zones", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --outfile=./dist/index.js --platform=node --target=node14 --external:aws-sdk index.ts", + "test": "", + "testreport": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "esbuild": "0.14.2", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1001.0" + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-route-53/associate-hosted-zones/tsconfig.json b/source/packages/@aws-accelerator/constructs/lib/aws-route-53/associate-hosted-zones/tsconfig.json new file mode 100644 index 000000000..ebcb98a79 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-route-53/associate-hosted-zones/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-route-53/hosted-zone.ts b/source/packages/@aws-accelerator/constructs/lib/aws-route-53/hosted-zone.ts new file mode 100644 index 000000000..02d241413 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-route-53/hosted-zone.ts @@ -0,0 +1,63 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +export interface IHostedZone extends cdk.IResource { + readonly hostedZoneId: string; + readonly hostedZoneName: string; + readonly vpcId: string; +} + +export interface HostedZoneProps { + readonly hostedZoneName: string; + readonly vpcId: string; +} + +export class HostedZone extends cdk.Resource implements IHostedZone { + readonly hostedZoneId: string; + readonly hostedZoneName: string; + readonly vpcId: string; + + constructor(scope: Construct, id: string, props: HostedZoneProps) { + super(scope, id); + + this.vpcId = props.vpcId; + this.hostedZoneName = props.hostedZoneName; + + const resource = new cdk.aws_route53.CfnHostedZone(this, 'Resource', { + name: props.hostedZoneName, + vpcs: [ + { + vpcId: this.vpcId, + vpcRegion: cdk.Stack.of(this).region, + }, + ], + }); + + this.hostedZoneId = resource.ref; + } + + static getHostedZoneNameForService(service: string, region: string): string { + let hostedZoneName = `${service}.${region}.amazonaws.com`; + const sagemakerArray = ['notebook', 'studio']; + if (sagemakerArray.includes(service)) { + hostedZoneName = `${service}.${region}.sagemaker.aws`; + } + if (service === 's3-global.accesspoint') { + hostedZoneName = `${service}.aws.com`; + } + return hostedZoneName; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-route-53/record-set.ts b/source/packages/@aws-accelerator/constructs/lib/aws-route-53/record-set.ts new file mode 100644 index 000000000..d1ef70cf8 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-route-53/record-set.ts @@ -0,0 +1,62 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +import { IHostedZone } from './hosted-zone'; + +export interface IRecordSet extends cdk.IResource { + readonly recordSetId: string; +} + +export interface RecordSetProps { + readonly type: string; + readonly name: string; + + readonly hostedZone: IHostedZone; + readonly dnsName?: string; + readonly hostedZoneId?: string; +} + +export class RecordSet extends cdk.Resource implements IRecordSet { + readonly recordSetId: string; + + constructor(scope: Construct, id: string, props: RecordSetProps) { + super(scope, id); + + const resource = new cdk.aws_route53.CfnRecordSet(this, 'Resource', { + type: props.type, + name: props.name, + hostedZoneId: props.hostedZone.hostedZoneId, + aliasTarget: { + dnsName: props.dnsName ?? '', + hostedZoneId: props.hostedZoneId ?? '', + }, + }); + + this.recordSetId = resource.ref; + } + + static getHostedZoneNameFromService(service: string, region: string): string { + let hostedZoneName = `${service}.${region}.amazonaws.com`; + const sagemakerArray = ['notebook', 'studio']; + if (sagemakerArray.includes(service)) { + hostedZoneName = `${service}.${region}.sagemaker.aws`; + } + if (service === 's3-global.accesspoint') { + hostedZoneName = `${service}.aws.com`; + } + return hostedZoneName; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-s3/bucket.ts b/source/packages/@aws-accelerator/constructs/lib/aws-s3/bucket.ts new file mode 100644 index 000000000..fb1ed892f --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-s3/bucket.ts @@ -0,0 +1,312 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as kms from 'aws-cdk-lib/aws-kms'; +import * as s3 from 'aws-cdk-lib/aws-s3'; +import { StorageClass } from '@aws-accelerator/config/lib/common-types/types'; +import { Construct } from 'constructs'; +import { pascalCase } from 'change-case'; + +export enum BucketAccessType { + READONLY = 'readonly', + WRITEONLY = 'writeonly', + READWRITE = 'readwrite', +} + +export enum BucketEncryptionType { + SSE_S3 = 'sse-s3', + SSE_KMS = 'sse-kms', +} + +interface Transition { + storageClass: StorageClass; + transitionAfter: number; +} + +export interface LifecycleRule { + abortIncompleteMultipartUploadAfter: number; + enabled: boolean; + expiration: number; + expiredObjectDeleteMarker: boolean; + id: string; + noncurrentVersionExpiration: number; + transitions: Transition[]; + noncurrentVersionTransitions: Transition[]; +} + +/** + * Construction properties for an S3 Bucket object. + */ +export interface BucketProps { + /** + * Physical name of this bucket. + * + * @default - Assigned by CloudFormation (recommended). + */ + s3BucketName?: string; + /** + * SSE encryption type for this bucket. + */ + encryptionType: BucketEncryptionType; + /** + * Policy to apply when the bucket is removed from this stack. + * + * @default - The bucket will be orphaned. + */ + s3RemovalPolicy?: cdk.RemovalPolicy; + /** + * The ksm key for bucket encryption. + */ + kmsKey?: kms.Key; + /** + * The name of the alias. + */ + kmsAliasName?: string; + /** + * A description of the key. + * + * Use a description that helps your users decide + * whether the key is appropriate for a particular task. + * + */ + kmsDescription?: string; + + /** + * + */ + serverAccessLogsBucket?: s3.IBucket | undefined; + + /** + * + */ + serverAccessLogsBucketName?: string; + + /** + * + */ + lifecycleRules?: LifecycleRule[]; + + /** + * Prefix to use in the target bucket for server access logs. + * + * @default - name of this bucket + */ + serverAccessLogsPrefix?: string; + + /** + * @optional + * A list of AWS principals and access type the bucket to grant + * principal should be a valid AWS resource principal like for AWS MacieSession it + * should be macie.amazonaws.com accessType should be any of these possible + * values BucketAccessType.READONLY, BucketAccessType.WRITEONLY, & and + * BucketAccessType.READWRITE + */ + awsPrincipalAccesses?: { principalAccesses: [{ principal: string; accessType: string }] }; +} + +/** + * Defines a Secure S3 Bucket object. By default a KMS CMK is generated and + * associated to the bucket. + */ +export class Bucket extends Construct { + private readonly bucket: s3.Bucket; + private readonly encryptionType: s3.BucketEncryption; + private readonly cmk?: kms.Key; + private readonly serverAccessLogsPrefix?: string; + + constructor(scope: Construct, id: string, props: BucketProps) { + super(scope, id); + + // Determine encryption type + if (props.encryptionType == BucketEncryptionType.SSE_KMS) { + if (props.kmsKey) { + this.cmk = props.kmsKey; + } else { + this.cmk = new kms.Key(this, 'Cmk', { + enableKeyRotation: true, + description: props.kmsDescription, + }); + if (props.kmsAliasName) { + this.cmk.addAlias(props.kmsAliasName); + } + } + this.encryptionType = s3.BucketEncryption.KMS; + } else if (props.encryptionType == BucketEncryptionType.SSE_S3) { + this.encryptionType = s3.BucketEncryption.S3_MANAGED; + } else { + throw new Error(`encryptionType ${props.encryptionType} is not valid.`); + } + + let serverAccessLogBucket: cdk.aws_s3.IBucket | undefined; + + if (props.serverAccessLogsBucketName && !props.serverAccessLogsBucket) { + serverAccessLogBucket = s3.Bucket.fromBucketName( + this, + `${pascalCase(props.serverAccessLogsBucketName)}-S3LogsBucket`, + props.serverAccessLogsBucketName, + ); + } + if (!props.serverAccessLogsBucketName && props.serverAccessLogsBucket) { + serverAccessLogBucket = props.serverAccessLogsBucket; + // Get server access logs prefix + if (!props.s3BucketName && !props.serverAccessLogsPrefix) { + throw new Error('s3BucketName or serverAccessLogsPrefix property must be defined when using serverAccessLogs.'); + } else { + this.serverAccessLogsPrefix = props.serverAccessLogsPrefix ? props.s3BucketName : props.s3BucketName; + } + } + if (props.serverAccessLogsBucketName && props.serverAccessLogsBucket) { + throw new Error('serverAccessLogsBucketName or serverAccessLogsBucket (only one property) should be defined.'); + } + + // Lifecycle rules + const lifecycleRules: cdk.aws_s3.LifecycleRule[] = []; + + if (props.lifecycleRules) { + for (const lifecycleRuleConfig of props.lifecycleRules) { + const transitions = []; + const noncurrentVersionTransitions = []; + + for (const transition of lifecycleRuleConfig.transitions) { + const transitionConfig = { + storageClass: new cdk.aws_s3.StorageClass(transition.storageClass), + transitionAfter: cdk.Duration.days(transition.transitionAfter), + }; + transitions.push(transitionConfig); + } + + for (const nonCurrentTransition of lifecycleRuleConfig.noncurrentVersionTransitions) { + const noncurrentVersionTransitionsConfig = { + storageClass: new cdk.aws_s3.StorageClass(nonCurrentTransition.storageClass), + transitionAfter: cdk.Duration.days(nonCurrentTransition.transitionAfter), + }; + noncurrentVersionTransitions.push(noncurrentVersionTransitionsConfig); + } + + lifecycleRules.push({ + abortIncompleteMultipartUploadAfter: cdk.Duration.days( + lifecycleRuleConfig.abortIncompleteMultipartUploadAfter, + ), + enabled: lifecycleRuleConfig.enabled, + expiration: cdk.Duration.days(lifecycleRuleConfig.expiration), + transitions, + noncurrentVersionTransitions, + noncurrentVersionExpiration: cdk.Duration.days(lifecycleRuleConfig.noncurrentVersionExpiration), + expiredObjectDeleteMarker: lifecycleRuleConfig.expiredObjectDeleteMarker, + id: `LifecycleRule${props.s3BucketName}`, + }); + } + } else { + lifecycleRules.push({ + abortIncompleteMultipartUploadAfter: cdk.Duration.days(1), + enabled: true, + expiration: cdk.Duration.days(1825), + expiredObjectDeleteMarker: false, + id: `LifecycleRule${props.s3BucketName}`, + noncurrentVersionExpiration: cdk.Duration.days(1825), + noncurrentVersionTransitions: [ + { + storageClass: cdk.aws_s3.StorageClass.DEEP_ARCHIVE, + transitionAfter: cdk.Duration.days(366), + }, + ], + transitions: [ + { + storageClass: cdk.aws_s3.StorageClass.DEEP_ARCHIVE, + transitionAfter: cdk.Duration.days(365), + }, + ], + }); + } + + this.bucket = new s3.Bucket(this, 'Resource', { + encryption: this.encryptionType, + encryptionKey: this.cmk, + removalPolicy: cdk.RemovalPolicy.RETAIN, + blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, + bucketName: props.s3BucketName, + versioned: true, + lifecycleRules, + objectOwnership: s3.ObjectOwnership.BUCKET_OWNER_PREFERRED, + serverAccessLogsBucket: serverAccessLogBucket, + // Trailing slash for folder-like prefix in S3 + serverAccessLogsPrefix: this.serverAccessLogsPrefix?.concat('/'), + }); + // Had to be removed to allow CloudTrail access + // this.bucket.addToResourcePolicy( + // new iam.PolicyStatement({ + // sid: 'deny-non-encrypted-object-uploads', + // effect: iam.Effect.DENY, + // actions: ['s3:PutObject'], + // resources: [this.bucket.arnForObjects('*')], + // principals: [new iam.AnyPrincipal()], + // conditions: { + // StringNotEquals: { + // 's3:x-amz-server-side-encryption': 'aws:kms', + // }, + // }, + // }), + // ); + this.bucket.addToResourcePolicy( + new iam.PolicyStatement({ + sid: 'deny-insecure-connections', + effect: iam.Effect.DENY, + actions: ['s3:*'], + resources: [this.bucket.bucketArn, this.bucket.arnForObjects('*')], + principals: [new iam.AnyPrincipal()], + conditions: { + Bool: { + 'aws:SecureTransport': 'false', + }, + }, + }), + ); + + // Add access policy for input AWS principal to the bucket + props.awsPrincipalAccesses?.principalAccesses.forEach(input => { + switch (input.accessType) { + case BucketAccessType.READONLY: + this.bucket.grantRead(new iam.ServicePrincipal(input.principal)); + break; + case BucketAccessType.WRITEONLY: + this.bucket.grantWrite(new iam.ServicePrincipal(input.principal)); + break; + case BucketAccessType.READWRITE: + this.bucket.grantReadWrite(new iam.ServicePrincipal(input.principal)); + break; + default: + throw new Error(`Invalid Access Type ${input.accessType} for ${input.principal} principal.`); + } + }); + } + + public getS3Bucket(): s3.IBucket { + return this.bucket; + } + + public getKey(): kms.Key { + if (this.cmk) { + return this.cmk; + } else { + throw new Error(`S3 bucket ${this.bucket.bucketName} has no associated CMK.`); + } + } + + protected addValidation(): string[] { + const errors: string[] = []; + + return errors; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-s3/central-logs-bucket.ts b/source/packages/@aws-accelerator/constructs/lib/aws-s3/central-logs-bucket.ts new file mode 100644 index 000000000..5cb05d5e8 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-s3/central-logs-bucket.ts @@ -0,0 +1,213 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import { StorageClass } from '@aws-accelerator/config/lib/common-types/types'; +import { Bucket, BucketEncryptionType } from '@aws-accelerator/constructs'; + +interface Transition { + storageClass: StorageClass; + transitionAfter: number; +} + +interface CentralLogBucketLifecycleRule { + abortIncompleteMultipartUploadAfter: number; + enabled: boolean; + expiration: number; + expiredObjectDeleteMarker: boolean; + id: string; + noncurrentVersionExpiration: number; + transitions: Transition[]; + noncurrentVersionTransitions: Transition[]; +} + +export interface CentralLogsBucketProps { + s3BucketName: string; + kmsAliasName: string; + kmsDescription: string; + serverAccessLogsBucket: Bucket; + organizationId?: string; + lifecycleRules?: CentralLogBucketLifecycleRule[]; +} + +/** + * Class to initialize Policy + */ +export class CentralLogsBucket extends Construct { + constructor(scope: Construct, id: string, props: CentralLogsBucketProps) { + super(scope, id); + + // Create Central Logs Bucket + const bucket = new Bucket(this, 'Resource', { + encryptionType: BucketEncryptionType.SSE_KMS, + s3BucketName: props.s3BucketName, + kmsAliasName: props.kmsAliasName, + kmsDescription: props.kmsDescription, + serverAccessLogsBucket: props.serverAccessLogsBucket.getS3Bucket(), + //lifecycleRules: props.lifecycleRules, + }); + + bucket.getKey().addToResourcePolicy( + new cdk.aws_iam.PolicyStatement({ + sid: 'Enable IAM User Permissions', + principals: [new cdk.aws_iam.AccountRootPrincipal()], + actions: ['kms:*'], + resources: ['*'], + }), + ); + + bucket.getS3Bucket().addToResourcePolicy( + new cdk.aws_iam.PolicyStatement({ + principals: [ + new cdk.aws_iam.ServicePrincipal('cloudtrail.amazonaws.com'), + new cdk.aws_iam.ServicePrincipal('config.amazonaws.com'), + new cdk.aws_iam.ServicePrincipal('delivery.logs.amazonaws.com'), + ], + actions: ['s3:PutObject'], + resources: [bucket.getS3Bucket().arnForObjects('*')], + conditions: { + StringEquals: { + 's3:x-amz-acl': 'bucket-owner-full-control', + }, + }, + }), + ); + + bucket.getS3Bucket().addToResourcePolicy( + new cdk.aws_iam.PolicyStatement({ + principals: [ + new cdk.aws_iam.ServicePrincipal('cloudtrail.amazonaws.com'), + new cdk.aws_iam.ServicePrincipal('config.amazonaws.com'), + new cdk.aws_iam.ServicePrincipal('delivery.logs.amazonaws.com'), + ], + actions: ['s3:GetBucketAcl', 's3:ListBucket'], + resources: [bucket.getS3Bucket().bucketArn], + }), + ); + + // // Permission to allow checking existence of AWSConfig bucket + // bucket.getS3Bucket().addToResourcePolicy( + // new iam.PolicyStatement({ + // principals: [new iam.ServicePrincipal('config.amazonaws.com')], + // actions: ['s3:ListBucket'], + // resources: [bucket.getS3Bucket().bucketArn], + // }), + // ); + + bucket.getS3Bucket().encryptionKey?.addToResourcePolicy( + new cdk.aws_iam.PolicyStatement({ + sid: 'Allow S3 use of the key', + actions: [ + 'kms:Decrypt', + 'kms:DescribeKey', + 'kms:Encrypt', + 'kms:GenerateDataKey', + 'kms:GenerateDataKeyWithoutPlaintext', + 'kms:GenerateRandom', + 'kms:GetKeyPolicy', + 'kms:GetKeyRotationStatus', + 'kms:ListAliases', + 'kms:ListGrants', + 'kms:ListKeyPolicies', + 'kms:ListKeys', + 'kms:ListResourceTags', + 'kms:ListRetirableGrants', + 'kms:ReEncryptFrom', + 'kms:ReEncryptTo', + ], + principals: [new cdk.aws_iam.ServicePrincipal('s3.amazonaws.com')], + resources: ['*'], + }), + ); + + bucket.getS3Bucket().encryptionKey?.addToResourcePolicy( + new cdk.aws_iam.PolicyStatement({ + sid: 'Allow AWS Services to encrypt and describe logs', + actions: [ + 'kms:Decrypt', + 'kms:DescribeKey', + 'kms:Encrypt', + 'kms:GenerateDataKey', + 'kms:GenerateDataKeyPair', + 'kms:GenerateDataKeyPairWithoutPlaintext', + 'kms:GenerateDataKeyWithoutPlaintext', + 'kms:ReEncryptFrom', + 'kms:ReEncryptTo', + ], + principals: [ + new cdk.aws_iam.ServicePrincipal('config.amazonaws.com'), + new cdk.aws_iam.ServicePrincipal('cloudtrail.amazonaws.com'), + new cdk.aws_iam.ServicePrincipal('delivery.logs.amazonaws.com'), + ], + resources: ['*'], + }), + ); + if (props.organizationId !== undefined) { + bucket.getS3Bucket().encryptionKey?.addToResourcePolicy( + new cdk.aws_iam.PolicyStatement({ + sid: 'Allow Organization use of the key', + actions: [ + 'kms:Decrypt', + 'kms:DescribeKey', + 'kms:Encrypt', + 'kms:GenerateDataKey', + 'kms:GenerateDataKeyPair', + 'kms:GenerateDataKeyPairWithoutPlaintext', + 'kms:GenerateDataKeyWithoutPlaintext', + 'kms:ReEncryptFrom', + 'kms:ReEncryptTo', + ], + principals: [new cdk.aws_iam.AnyPrincipal()], + resources: ['*'], + conditions: { + StringEquals: { + 'aws:PrincipalOrgID': props.organizationId, + }, + }, + }), + ); + + const centralLogBucketKmsKeyArnSsmParameter = new cdk.aws_ssm.StringParameter( + this, + 'SsmParamCentralAccountBucketKMSArn', + { + parameterName: '/accelerator/logging/central-bucket/kms/arn', + stringValue: bucket.getKey().keyArn, + }, + ); + + // SSM parameter access IAM Role for + new cdk.aws_iam.Role(this, 'CrossAccountCentralBucketKMSArnSsmParamAccessRole', { + roleName: `AWSAccelerator-CentralBucketKMSArnSsmParam-${cdk.Stack.of(this).region}`, + assumedBy: new cdk.aws_iam.OrganizationPrincipal(props.organizationId), + inlinePolicies: { + default: new cdk.aws_iam.PolicyDocument({ + statements: [ + new cdk.aws_iam.PolicyStatement({ + effect: cdk.aws_iam.Effect.ALLOW, + actions: ['ssm:GetParameters', 'ssm:GetParameter'], + resources: [centralLogBucketKmsKeyArnSsmParameter.parameterArn], + }), + new cdk.aws_iam.PolicyStatement({ + effect: cdk.aws_iam.Effect.ALLOW, + actions: ['ssm:DescribeParameters'], + resources: ['*'], + }), + ], + }), + }, + }); + } + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-s3/public-access-block.ts b/source/packages/@aws-accelerator/constructs/lib/aws-s3/public-access-block.ts new file mode 100644 index 000000000..cdf4b9f87 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-s3/public-access-block.ts @@ -0,0 +1,91 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +const path = require('path'); + +export interface S3PublicAccessBlockProps { + blockPublicAcls: boolean; + blockPublicPolicy: boolean; + ignorePublicAcls: boolean; + restrictPublicBuckets: boolean; + /** + * @default cdk.Aws.ACCOUNT_ID + */ + accountId?: string; + /** + * Custom resource lambda log group encryption key + */ + readonly kmsKey: cdk.aws_kms.Key; + /** + * Custom resource lambda log retention in days + */ + readonly logRetentionInDays: number; +} + +/** + * Class to initialize Policy + */ +export class S3PublicAccessBlock extends Construct { + readonly id: string; + + constructor(scope: Construct, id: string, props: S3PublicAccessBlockProps) { + super(scope, id); + + // + // Function definition for the custom resource + // + const provider = cdk.CustomResourceProvider.getOrCreateProvider(this, 'Custom::S3PutPublicAccessBlock', { + codeDirectory: path.join(__dirname, 'put-public-access-block/dist'), + runtime: cdk.CustomResourceProviderRuntime.NODEJS_14_X, + policyStatements: [ + { + Effect: 'Allow', + Action: ['s3:PutAccountPublicAccessBlock'], + Resource: '*', + }, + ], + }); + + const resource = new cdk.CustomResource(this, 'Resource', { + resourceType: 'Custom::PutPublicAccessBlock', + serviceToken: provider.serviceToken, + properties: { + blockPublicAcls: props.blockPublicAcls, + blockPublicPolicy: props.blockPublicPolicy, + ignorePublicAcls: props.ignorePublicAcls, + restrictPublicBuckets: props.restrictPublicBuckets, + accountId: props.accountId, + }, + }); + + /** + * Singleton pattern to define the log group for the singleton function + * in the stack + */ + const stack = cdk.Stack.of(scope); + const logGroup = + (stack.node.tryFindChild(`${provider.node.id}LogGroup`) as cdk.aws_logs.LogGroup) ?? + new cdk.aws_logs.LogGroup(stack, `${provider.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${(provider.node.findChild('Handler') as cdk.aws_lambda.CfnFunction).ref}`, + retention: props.logRetentionInDays, + encryptionKey: props.kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + resource.node.addDependency(logGroup); + + this.id = resource.ref; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-s3/put-public-access-block/index.ts b/source/packages/@aws-accelerator/constructs/lib/aws-s3/put-public-access-block/index.ts new file mode 100644 index 000000000..4dc563964 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-s3/put-public-access-block/index.ts @@ -0,0 +1,67 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { throttlingBackOff } from '@aws-accelerator/utils'; +import * as AWS from 'aws-sdk'; +AWS.config.logger = console; + +/** + * put-public-access-block - lambda handler + * + * @param event + * @returns + */ +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + PhysicalResourceId: string | undefined; + Status: string; + } + | undefined +> { + const accountId: string = event.ResourceProperties['accountId']; + const blockPublicAcls: boolean = event.ResourceProperties['blockPublicAcls'] === 'true'; + const blockPublicPolicy: boolean = event.ResourceProperties['blockPublicPolicy'] === 'true'; + const ignorePublicAcls: boolean = event.ResourceProperties['ignorePublicAcls'] === 'true'; + const restrictPublicBuckets: boolean = event.ResourceProperties['restrictPublicBuckets'] === 'true'; + + const s3ControlClient = new AWS.S3Control({}); + + switch (event.RequestType) { + case 'Create': + case 'Update': + await throttlingBackOff(() => + s3ControlClient + .putPublicAccessBlock({ + AccountId: accountId, + PublicAccessBlockConfiguration: { + BlockPublicAcls: blockPublicAcls, + BlockPublicPolicy: blockPublicPolicy, + IgnorePublicAcls: ignorePublicAcls, + RestrictPublicBuckets: restrictPublicBuckets, + }, + }) + .promise(), + ); + return { + PhysicalResourceId: `s3-bpa-${accountId}`, + Status: 'SUCCESS', + }; + + case 'Delete': + // Do Nothing + return { + PhysicalResourceId: event.PhysicalResourceId, + Status: 'SUCCESS', + }; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-s3/put-public-access-block/package.json b/source/packages/@aws-accelerator/constructs/lib/aws-s3/put-public-access-block/package.json new file mode 100644 index 000000000..5cf3f03e7 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-s3/put-public-access-block/package.json @@ -0,0 +1,43 @@ +{ + "name": "@aws-accelerator/constructs-aws-s3-put-public-access-block", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --outfile=./dist/index.js --platform=node --target=node14 --external:aws-sdk index.ts", + "test": "", + "testreport": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "esbuild": "0.14.2", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1001.0" + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-s3/put-public-access-block/tsconfig.json b/source/packages/@aws-accelerator/constructs/lib/aws-s3/put-public-access-block/tsconfig.json new file mode 100644 index 000000000..ebcb98a79 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-s3/put-public-access-block/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-securityhub/batch-enable-standards/index.ts b/source/packages/@aws-accelerator/constructs/lib/aws-securityhub/batch-enable-standards/index.ts new file mode 100644 index 000000000..5e3649490 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-securityhub/batch-enable-standards/index.ts @@ -0,0 +1,334 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { throttlingBackOff } from '@aws-accelerator/utils'; +import * as AWS from 'aws-sdk'; +AWS.config.logger = console; + +/** + * batch-enable-standards - lambda handler + * + * @param event + * @returns + */ +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + Status: string | undefined; + StatusCode: number | undefined; + } + | undefined +> { + const region = event.ResourceProperties['region']; + const inputStandards: { name: string; enable: boolean; controlsToDisable: string[] | undefined }[] = + event.ResourceProperties['standards']; + + const securityHubClient = new AWS.SecurityHub({ region: region }); + + // Get AWS defined security standards name and ARN + const awsSecurityHubStandards: { [name: string]: string }[] = []; + let nextToken: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => securityHubClient.describeStandards({ NextToken: nextToken }).promise()); + for (const standard of page.Standards ?? []) { + if (standard.StandardsArn && standard.Name) { + const securityHubStandard: { [name: string]: string } = {}; + securityHubStandard[standard.Name] = standard.StandardsArn; + awsSecurityHubStandards.push(securityHubStandard); + } + } + nextToken = page.NextToken; + } while (nextToken); + + // Enable security hub is admin account before creating delegation admin account, if this wasn't enabled by organization delegation + await enableSecurityHub(securityHubClient); + + const standardsModificationList = await getStandardsModificationList( + securityHubClient, + inputStandards, + awsSecurityHubStandards, + ); + console.log('standardsModificationList'); + console.log(standardsModificationList); + + switch (event.RequestType) { + case 'Create': + case 'Update': + console.log('starting - BatchEnableStandardsCommand'); + + // When there are standards to be enable + if (standardsModificationList.toEnableStandardRequests.length > 0) { + console.log('to enable'); + console.log(standardsModificationList.toEnableStandardRequests); + await throttlingBackOff(() => + securityHubClient + .batchEnableStandards({ + StandardsSubscriptionRequests: standardsModificationList.toEnableStandardRequests, + }) + .promise(), + ); + } + + // When there are standards to be disable + if (standardsModificationList.toDisableStandardArns!.length > 0) { + console.log('to disable'); + console.log(standardsModificationList.toEnableStandardRequests); + await throttlingBackOff(() => + securityHubClient + .batchDisableStandards({ + StandardsSubscriptionArns: standardsModificationList.toDisableStandardArns!, + }) + .promise(), + ); + } + + // get list of controls to modify + const controlsToModify = await getControlArnsToModify(securityHubClient, inputStandards, awsSecurityHubStandards); + + // Enable standard controls + for (const controlArnToModify of controlsToModify.disableStandardControlArns) { + await throttlingBackOff(() => + securityHubClient + .updateStandardsControl({ + StandardsControlArn: controlArnToModify, + ControlStatus: 'DISABLED', + DisabledReason: 'Control disabled by Accelerator', + }) + .promise(), + ); + } + + // Disable standard controls + for (const controlArnToModify of controlsToModify.enableStandardControlArns) { + await throttlingBackOff(() => + securityHubClient + .updateStandardsControl({ StandardsControlArn: controlArnToModify, ControlStatus: 'ENABLED' }) + .promise(), + ); + } + + return { Status: 'Success', StatusCode: 200 }; + + case 'Delete': + const existingEnabledStandards = await getExistingEnabledStandards(securityHubClient); + const subscriptionArns: string[] = []; + existingEnabledStandards.forEach(standard => { + subscriptionArns.push(standard.StandardsSubscriptionArn); + }); + + if (subscriptionArns.length > 0) { + console.log('Below listed standards disable during delete'); + console.log(subscriptionArns); + await throttlingBackOff(() => + securityHubClient.batchDisableStandards({ StandardsSubscriptionArns: subscriptionArns }).promise(), + ); + } + + return { Status: 'Success', StatusCode: 200 }; + } +} + +/** + * Enable SecurityHub + * @param securityHubClient + */ +async function enableSecurityHub(securityHubClient: AWS.SecurityHub): Promise { + try { + await throttlingBackOff(() => securityHubClient.enableSecurityHub({ EnableDefaultStandards: false }).promise()); + } catch ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + e: any + ) { + if ( + // SDKv2 Error Structure + e.code === 'ResourceConflictException' || + // SDKv3 Error Structure + e.name === 'ResourceConflictException' + ) { + console.warn(e.name + ': ' + e.message); + return; + } + throw new Error(`SecurityHub enable issue error message - ${e}`); + } +} + +/** + * Function to provide existing enabled standards + * @param securityHubClient + */ +async function getExistingEnabledStandards( + securityHubClient: AWS.SecurityHub, +): Promise { + const response = await throttlingBackOff(() => securityHubClient.getEnabledStandards({}).promise()); + + // Get list of existing enabled standards within securityhub + const existingEnabledStandardArns: AWS.SecurityHub.StandardsSubscription[] = []; + response.StandardsSubscriptions!.forEach(item => { + // if (item.StandardsStatus === StandardsStatus.READY) { + existingEnabledStandardArns.push({ + StandardsArn: item.StandardsArn!, + StandardsInput: item.StandardsInput!, + StandardsStatus: item.StandardsStatus!, + StandardsSubscriptionArn: item.StandardsSubscriptionArn!, + }); + // } + }); + + return existingEnabledStandardArns; +} + +/** + * Function to provide list of control arns for standards to be enable or disable + * @param securityHubClient + * @param inputStandards + * @param awsSecurityHubStandards + */ +async function getControlArnsToModify( + securityHubClient: AWS.SecurityHub, + inputStandards: { name: string; enable: boolean; controlsToDisable: string[] | undefined }[], + awsSecurityHubStandards: { [name: string]: string }[], +): Promise<{ disableStandardControlArns: string[]; enableStandardControlArns: string[] }> { + const existingEnabledStandards = await getExistingEnabledStandards(securityHubClient); + const disableStandardControls: string[] = []; + const enableStandardControls: string[] = []; + + for (const inputStandard of inputStandards) { + if (inputStandard.enable) { + for (const awsSecurityHubStandard of awsSecurityHubStandards) { + if (awsSecurityHubStandard[inputStandard.name]) { + const existingEnabledStandard = existingEnabledStandards.find( + item => item.StandardsArn === awsSecurityHubStandard[inputStandard.name], + ); + if (existingEnabledStandard) { + console.log(`Getting controls for ${existingEnabledStandard?.StandardsSubscriptionArn} subscription`); + + const standardsControl: AWS.SecurityHub.StandardsControl[] = []; + + let nextToken: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => + securityHubClient + .describeStandardsControls({ + StandardsSubscriptionArn: existingEnabledStandard?.StandardsSubscriptionArn, + NextToken: nextToken, + }) + .promise(), + ); + for (const control of page.Controls ?? []) { + standardsControl.push(control); + } + nextToken = page.NextToken; + } while (nextToken); + + while (standardsControl.length === 0) { + console.warn( + `Delaying standard control retrieval by 10000 ms for ${existingEnabledStandard?.StandardsSubscriptionArn}`, + ); + await delay(10000); + console.warn(`Rechecking - Getting controls for ${existingEnabledStandard?.StandardsSubscriptionArn}`); + let nextToken: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => + securityHubClient + .describeStandardsControls({ + StandardsSubscriptionArn: existingEnabledStandard?.StandardsSubscriptionArn, + NextToken: nextToken, + }) + .promise(), + ); + for (const control of page.Controls ?? []) { + standardsControl.push(control); + } + nextToken = page.NextToken; + } while (nextToken); + } + + console.log(`When control list available for ${existingEnabledStandard?.StandardsSubscriptionArn}`); + console.log(standardsControl); + + for (const control of standardsControl) { + if (inputStandard.controlsToDisable?.includes(control.ControlId!)) { + console.log(control.ControlId!); + console.log(inputStandard.name); + disableStandardControls.push(control.StandardsControlArn!); + } else { + if (control.ControlStatus == 'DISABLED') { + console.log('following is disabled need to be enable now'); + console.log(control.ControlId!); + enableStandardControls.push(control.StandardsControlArn!); + } + } + } + } + } + } + } + } + console.log('***********'); + console.log(disableStandardControls); + console.log(enableStandardControls); + console.log('***********'); + + return { disableStandardControlArns: disableStandardControls, enableStandardControlArns: enableStandardControls }; +} + +/** + * Function to be executed before event specific action starts, this function makes the list of standards to be enable or disable based on the input + * @param securityHubClient + * @param inputStandards + * @param awsSecurityHubStandards + */ +async function getStandardsModificationList( + securityHubClient: AWS.SecurityHub, + inputStandards: { name: string; enable: boolean; controlsToDisable: string[] | undefined }[], + awsSecurityHubStandards: { [name: string]: string }[], +): Promise<{ + toEnableStandardRequests: AWS.SecurityHub.StandardsSubscriptionRequests; + toDisableStandardArns: string[] | undefined; +}> { + const existingEnabledStandards = await getExistingEnabledStandards(securityHubClient); + const toEnableStandardRequests: AWS.SecurityHub.StandardsSubscriptionRequests = []; + const toDisableStandardArns: string[] | undefined = []; + + for (const inputStandard of inputStandards) { + if (inputStandard.enable) { + for (const awsSecurityHubStandard of awsSecurityHubStandards) { + if (awsSecurityHubStandard[inputStandard.name]) { + const existingEnabledStandard = existingEnabledStandards.filter( + item => item.StandardsArn === awsSecurityHubStandard[inputStandard.name], + ); + if (existingEnabledStandard.length === 0) { + toEnableStandardRequests.push({ StandardsArn: awsSecurityHubStandard[inputStandard.name] }); + } + } + } + } else { + for (const awsSecurityHubStandard of awsSecurityHubStandards) { + if (awsSecurityHubStandard[inputStandard.name]) { + const existingEnabledStandard = existingEnabledStandards.find( + item => item.StandardsArn === awsSecurityHubStandard[inputStandard.name], + ); + + if (existingEnabledStandard) { + toDisableStandardArns.push(existingEnabledStandard?.StandardsSubscriptionArn); + } + } + } + } + } + + return { toEnableStandardRequests: toEnableStandardRequests, toDisableStandardArns: toDisableStandardArns }; +} + +async function delay(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-securityhub/batch-enable-standards/package.json b/source/packages/@aws-accelerator/constructs/lib/aws-securityhub/batch-enable-standards/package.json new file mode 100644 index 000000000..8015af5cc --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-securityhub/batch-enable-standards/package.json @@ -0,0 +1,43 @@ +{ + "name": "@aws-accelerator/constructs-aws-securityhub-batch-enable-standards", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --outfile=./dist/index.js --platform=node --target=node14 --external:aws-sdk index.ts", + "test": "", + "testreport": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "esbuild": "0.14.2", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1001.0" + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-securityhub/batch-enable-standards/tsconfig.json b/source/packages/@aws-accelerator/constructs/lib/aws-securityhub/batch-enable-standards/tsconfig.json new file mode 100644 index 000000000..272282f1f --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-securityhub/batch-enable-standards/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} \ No newline at end of file diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-securityhub/create-members/index.ts b/source/packages/@aws-accelerator/constructs/lib/aws-securityhub/create-members/index.ts new file mode 100644 index 000000000..6c56b26b2 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-securityhub/create-members/index.ts @@ -0,0 +1,114 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { throttlingBackOff } from '@aws-accelerator/utils'; +import * as AWS from 'aws-sdk'; +AWS.config.logger = console; + +/** + * enable-guardduty - lambda handler + * + * @param event + * @returns + */ +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + Status: string | undefined; + StatusCode: number | undefined; + } + | undefined +> { + const region = event.ResourceProperties['region']; + const partition = event.ResourceProperties['partition']; + + let organizationsClient: AWS.Organizations; + if (partition === 'aws-us-gov') { + organizationsClient = new AWS.Organizations({ region: 'us-gov-west-1' }); + } else { + organizationsClient = new AWS.Organizations({ region: 'us-east-1' }); + } + const securityHubClient = new AWS.SecurityHub({ region: region }); + + // Enable security hub is admin account before creating delegation admin account, if this wasn't enabled by organization delegation + await enableSecurityHub(securityHubClient); + + const allAccounts: AWS.SecurityHub.AccountDetails[] = []; + let nextToken: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => organizationsClient.listAccounts({ NextToken: nextToken }).promise()); + for (const account of page.Accounts ?? []) { + allAccounts.push({ AccountId: account.Id!, Email: account.Email }); + } + nextToken = page.NextToken; + } while (nextToken); + + switch (event.RequestType) { + case 'Create': + case 'Update': + console.log('starting - CreateMembersCommand'); + + await throttlingBackOff(() => securityHubClient.createMembers({ AccountDetails: allAccounts }).promise()); + + await throttlingBackOff(() => securityHubClient.updateOrganizationConfiguration({ AutoEnable: true }).promise()); + + return { Status: 'Success', StatusCode: 200 }; + + case 'Delete': + const existingMemberAccountIds: string[] = []; + let nextToken: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => securityHubClient.listMembers({ NextToken: nextToken }).promise()); + for (const member of page.Members ?? []) { + console.log(member); + existingMemberAccountIds.push(member.AccountId!); + } + nextToken = page.NextToken; + } while (nextToken); + + if (existingMemberAccountIds.length > 0) { + await throttlingBackOff(() => + securityHubClient.disassociateMembers({ AccountIds: existingMemberAccountIds }).promise(), + ); + + await throttlingBackOff(() => + securityHubClient.deleteMembers({ AccountIds: existingMemberAccountIds }).promise(), + ); + } + + return { Status: 'Success', StatusCode: 200 }; + } +} + +/** + * Enable SecurityHub + * @param securityHubClient + */ +async function enableSecurityHub(securityHubClient: AWS.SecurityHub): Promise { + try { + await throttlingBackOff(() => securityHubClient.enableSecurityHub({ EnableDefaultStandards: false }).promise()); + } catch ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + e: any + ) { + if ( + // SDKv2 Error Structure + e.code === 'ResourceConflictException' || + // SDKv3 Error Structure + e.name === 'ResourceConflictException' + ) { + console.warn(e.name + ': ' + e.message); + return; + } + throw new Error(`SecurityHub enable issue error message - ${e}`); + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-securityhub/create-members/package.json b/source/packages/@aws-accelerator/constructs/lib/aws-securityhub/create-members/package.json new file mode 100644 index 000000000..c44ed9c9c --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-securityhub/create-members/package.json @@ -0,0 +1,43 @@ +{ + "name": "@aws-accelerator/constructs-aws-securityhub-create-members", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --outfile=./dist/index.js --platform=node --target=node14 --external:aws-sdk index.ts", + "test": "", + "testreport": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "esbuild": "0.14.2", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1001.0" + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-securityhub/create-members/tsconfig.json b/source/packages/@aws-accelerator/constructs/lib/aws-securityhub/create-members/tsconfig.json new file mode 100644 index 000000000..272282f1f --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-securityhub/create-members/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} \ No newline at end of file diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-securityhub/enable-organization-admin-account/index.ts b/source/packages/@aws-accelerator/constructs/lib/aws-securityhub/enable-organization-admin-account/index.ts new file mode 100644 index 000000000..afcf2b47f --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-securityhub/enable-organization-admin-account/index.ts @@ -0,0 +1,171 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { throttlingBackOff } from '@aws-accelerator/utils'; +import * as AWS from 'aws-sdk'; +AWS.config.logger = console; + +/** + * SecurityHubOrganizationAdminAccount - lambda handler + * + * @param event + * @returns + */ +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + Status: string | undefined; + StatusCode: number | undefined; + } + | undefined +> { + const region = event.ResourceProperties['region']; + const partition = event.ResourceProperties['partition']; + const adminAccountId = event.ResourceProperties['adminAccountId']; + + let organizationsClient: AWS.Organizations; + if (partition === 'aws-us-gov') { + organizationsClient = new AWS.Organizations({ region: 'us-gov-west-1' }); + } else { + organizationsClient = new AWS.Organizations({ region: 'us-east-1' }); + } + const securityHubClient = new AWS.SecurityHub({ region: region }); + + const securityHubAdminAccount = await getSecurityHubDelegatedAccount(securityHubClient, adminAccountId); + + switch (event.RequestType) { + case 'Create': + case 'Update': + if (securityHubAdminAccount.status) { + if (securityHubAdminAccount.accountId === adminAccountId) { + console.warn( + `SecurityHub admin account ${securityHubAdminAccount.accountId} is already an admin account as status is ${securityHubAdminAccount.status}, in ${region} region. No action needed`, + ); + return { Status: 'Success', StatusCode: 200 }; + } else { + console.warn( + `SecurityHub delegated admin is already set to ${securityHubAdminAccount.accountId} account can not assign another delegated account`, + ); + } + } else { + // Enable security hub in management account before creating delegation admin account + await enableSecurityHub(securityHubClient); + console.log( + `Started enableOrganizationAdminAccount function in ${event.ResourceProperties['region']} region for account ${adminAccountId}`, + ); + await throttlingBackOff(() => + securityHubClient.enableOrganizationAdminAccount({ AdminAccountId: adminAccountId }).promise(), + ); + } + + return { Status: 'Success', StatusCode: 200 }; + + case 'Delete': + if (securityHubAdminAccount.accountId) { + if (securityHubAdminAccount.accountId === adminAccountId) { + console.log( + `Started disableOrganizationAdminAccount function in ${event.ResourceProperties['region']} region for account ${adminAccountId}`, + ); + await throttlingBackOff(() => + securityHubClient.disableOrganizationAdminAccount({ AdminAccountId: adminAccountId }).promise(), + ); + const response = await throttlingBackOff(() => + organizationsClient + .listDelegatedAdministrators({ ServicePrincipal: 'securityhub.amazonaws.com' }) + .promise(), + ); + + if (response.DelegatedAdministrators!.length > 0) { + console.log( + `Started deregisterDelegatedAdministrator function in ${event.ResourceProperties['region']} region for account ${adminAccountId}`, + ); + await throttlingBackOff(() => + organizationsClient + .deregisterDelegatedAdministrator({ + AccountId: adminAccountId, + ServicePrincipal: 'securityhub.amazonaws.com', + }) + .promise(), + ); + } else { + console.warn( + `Account ${securityHubAdminAccount.accountId} is not registered as delegated administrator account`, + ); + } + } + } else { + console.warn( + `SecurityHub delegation is not configured for account ${securityHubAdminAccount.accountId}, no action performed`, + ); + } + return { Status: 'Success', StatusCode: 200 }; + } +} + +/** + * Find SecurityHub delegated account Id + * @param securityHubClient + * @param adminAccountId + */ +async function getSecurityHubDelegatedAccount( + securityHubClient: AWS.SecurityHub, + adminAccountId: string, +): Promise<{ accountId: string | undefined; status: string | undefined }> { + const adminAccounts: AWS.SecurityHub.AdminAccount[] = []; + let nextToken: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => + securityHubClient.listOrganizationAdminAccounts({ NextToken: nextToken }).promise(), + ); + for (const account of page.AdminAccounts ?? []) { + adminAccounts.push(account); + } + nextToken = page.NextToken; + } while (nextToken); + + if (adminAccounts.length === 0) { + return { accountId: undefined, status: undefined }; + } + if (adminAccounts.length > 1) { + throw new Error('Multiple admin accounts for SecurityHub in organization'); + } + + if (adminAccounts[0].AccountId === adminAccountId && adminAccounts[0].Status === 'DISABLE_IN_PROGRESS') { + throw new Error(`Admin account ${adminAccounts[0].AccountId} is in ${adminAccounts[0].Status}`); + } + + return { accountId: adminAccounts[0].AccountId, status: adminAccounts[0].Status }; +} + +/** + * Enable SecurityHub + * @param securityHubClient + */ +async function enableSecurityHub(securityHubClient: AWS.SecurityHub): Promise { + try { + await throttlingBackOff(() => securityHubClient.enableSecurityHub({ EnableDefaultStandards: false }).promise()); + } catch ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + e: any + ) { + if ( + // SDKv2 Error Structure + e.code === 'ResourceConflictException' || + // SDKv3 Error Structure + e.name === 'ResourceConflictException' + ) { + console.warn(e.name + ': ' + e.message); + return; + } + throw new Error(`SecurityHub enable issue error message - ${e}`); + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-securityhub/enable-organization-admin-account/package.json b/source/packages/@aws-accelerator/constructs/lib/aws-securityhub/enable-organization-admin-account/package.json new file mode 100644 index 000000000..1bf6e7978 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-securityhub/enable-organization-admin-account/package.json @@ -0,0 +1,43 @@ +{ + "name": "@aws-accelerator/constructs-aws-securityhub-enable-organization-admin-account", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --outfile=./dist/index.js --platform=node --target=node14 --external:aws-sdk index.ts", + "test": "", + "testreport": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "esbuild": "0.14.2", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1001.0" + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-securityhub/enable-organization-admin-account/tsconfig.json b/source/packages/@aws-accelerator/constructs/lib/aws-securityhub/enable-organization-admin-account/tsconfig.json new file mode 100644 index 000000000..272282f1f --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-securityhub/enable-organization-admin-account/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} \ No newline at end of file diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-securityhub/securityhub-members.ts b/source/packages/@aws-accelerator/constructs/lib/aws-securityhub/securityhub-members.ts new file mode 100644 index 000000000..d2b2e65d8 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-securityhub/securityhub-members.ts @@ -0,0 +1,102 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +const path = require('path'); + +/** + * Initialized SecurityHubMembersProps properties + */ +export interface SecurityHubMembersProps { + /** + * Custom resource lambda log group encryption key + */ + readonly kmsKey: cdk.aws_kms.Key; + /** + * Custom resource lambda log retention in days + */ + readonly logRetentionInDays: number; +} + +/** + /** + * Class - SecurityHubMembers + */ +export class SecurityHubMembers extends Construct { + public readonly id: string; + + constructor(scope: Construct, id: string, props: SecurityHubMembersProps) { + super(scope, id); + + const RESOURCE_TYPE = 'Custom::SecurityHubCreateMembers'; + + const provider = cdk.CustomResourceProvider.getOrCreateProvider(this, RESOURCE_TYPE, { + codeDirectory: path.join(__dirname, 'create-members/dist'), + runtime: cdk.CustomResourceProviderRuntime.NODEJS_14_X, + policyStatements: [ + { + Sid: 'SecurityHubCreateMembersTaskOrganizationAction', + Effect: 'Allow', + Action: ['organizations:ListAccounts'], + Resource: '*', + Condition: { + StringLikeIfExists: { + 'organizations:ListAccounts': ['securityhub.amazonaws.com'], + }, + }, + }, + { + Sid: 'SecurityHubCreateMembersTaskSecurityHubActions', + Effect: 'Allow', + Action: [ + 'securityhub:CreateMembers', + 'securityhub:DeleteMembers', + 'securityhub:DisassociateMembers', + 'securityhub:EnableSecurityHub', + 'securityhub:ListMembers', + 'securityhub:UpdateOrganizationConfiguration', + ], + Resource: '*', + }, + ], + }); + + const resource = new cdk.CustomResource(this, 'Resource', { + resourceType: RESOURCE_TYPE, + serviceToken: provider.serviceToken, + properties: { + region: cdk.Stack.of(this).region, + partition: cdk.Aws.PARTITION, + }, + }); + + /** + * Singleton pattern to define the log group for the singleton function + * in the stack + */ + const stack = cdk.Stack.of(scope); + const logGroup = + (stack.node.tryFindChild(`${provider.node.id}LogGroup`) as cdk.aws_logs.LogGroup) ?? + new cdk.aws_logs.LogGroup(stack, `${provider.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${(provider.node.findChild('Handler') as cdk.aws_lambda.CfnFunction).ref}`, + retention: props.logRetentionInDays, + encryptionKey: props.kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + resource.node.addDependency(logGroup); + + this.id = resource.ref; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-securityhub/securityhub-organization-admin-account.ts b/source/packages/@aws-accelerator/constructs/lib/aws-securityhub/securityhub-organization-admin-account.ts new file mode 100644 index 000000000..29bb1102d --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-securityhub/securityhub-organization-admin-account.ts @@ -0,0 +1,134 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +const path = require('path'); + +/** + * Initialized SecurityHubOrganizationalAdminAccountProps properties + */ +export interface SecurityHubOrganizationalAdminAccountProps { + /** + * Admin account id + */ + readonly adminAccountId: string; + /** + * Custom resource lambda log group encryption key + */ + readonly kmsKey: cdk.aws_kms.Key; + /** + * Custom resource lambda log retention in days + */ + readonly logRetentionInDays: number; +} + +/** + * Class - SecurityHubOrganizationAdminAccount + */ +export class SecurityHubOrganizationAdminAccount extends Construct { + public readonly id: string; + + constructor(scope: Construct, id: string, props: SecurityHubOrganizationalAdminAccountProps) { + super(scope, id); + + const RESOURCE_TYPE = 'Custom::SecurityHubEnableOrganizationAdminAccount'; + + const provider = cdk.CustomResourceProvider.getOrCreateProvider(this, RESOURCE_TYPE, { + codeDirectory: path.join(__dirname, 'enable-organization-admin-account/dist'), + runtime: cdk.CustomResourceProviderRuntime.NODEJS_14_X, + policyStatements: [ + { + Sid: 'SecurityHubEnableOrganizationAdminAccountTaskOrganizationActions', + Effect: 'Allow', + Action: [ + 'organizations:DescribeOrganization', + 'organizations:ListAccounts', + 'organizations:ListDelegatedAdministrators', + ], + Resource: '*', + }, + { + Effect: 'Allow', + Action: 'organizations:EnableAWSServiceAccess', + Resource: '*', + Condition: { + StringEquals: { + 'organizations:ServicePrincipal': 'securityhub.amazonaws.com', + }, + }, + }, + { + Effect: 'Allow', + Action: ['organizations:RegisterDelegatedAdministrator', 'organizations:DeregisterDelegatedAdministrator'], + Resource: `arn:${cdk.Stack.of(this).partition}:organizations::*:account/o-*/*`, + Condition: { + StringEquals: { + 'organizations:ServicePrincipal': 'securityhub.amazonaws.com', + }, + }, + }, + { + Sid: 'SecurityHubCreateMembersTaskIamAction', + Effect: 'Allow', + Action: ['iam:CreateServiceLinkedRole'], + Resource: '*', + Condition: { + StringLike: { + 'iam:AWSServiceName': ['securityhub.amazonaws.com'], + }, + }, + }, + { + Sid: 'SecurityHubEnableOrganizationAdminAccountTaskSecurityHubActions', + Effect: 'Allow', + Action: [ + 'securityhub:DisableOrganizationAdminAccount', + 'securityhub:EnableOrganizationAdminAccount', + 'securityhub:EnableSecurityHub', + 'securityhub:ListOrganizationAdminAccounts', + ], + Resource: '*', + }, + ], + }); + + const resource = new cdk.CustomResource(this, 'Resource', { + resourceType: RESOURCE_TYPE, + serviceToken: provider.serviceToken, + properties: { + region: cdk.Stack.of(this).region, + partition: cdk.Aws.PARTITION, + adminAccountId: props.adminAccountId, + }, + }); + + /** + * Singleton pattern to define the log group for the singleton function + * in the stack + */ + const stack = cdk.Stack.of(scope); + const logGroup = + (stack.node.tryFindChild(`${provider.node.id}LogGroup`) as cdk.aws_logs.LogGroup) ?? + new cdk.aws_logs.LogGroup(stack, `${provider.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${(provider.node.findChild('Handler') as cdk.aws_lambda.CfnFunction).ref}`, + retention: props.logRetentionInDays, + encryptionKey: props.kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + resource.node.addDependency(logGroup); + + this.id = resource.ref; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-securityhub/securityhub-standards.ts b/source/packages/@aws-accelerator/constructs/lib/aws-securityhub/securityhub-standards.ts new file mode 100644 index 000000000..97aa09ef6 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-securityhub/securityhub-standards.ts @@ -0,0 +1,96 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +const path = require('path'); + +/** + * Initialized SecurityHubMembersProps properties + */ +export interface SecurityHubStandardsProps { + /** + * Security hun standard + */ + readonly standards: { name: string; enable: boolean; controlsToDisable: string[] | undefined }[]; + /** + * Custom resource lambda log group encryption key + */ + readonly kmsKey: cdk.aws_kms.Key; + /** + * Custom resource lambda log retention in days + */ + readonly logRetentionInDays: number; +} + +/** + /** + * Class - SecurityHubMembers + */ +export class SecurityHubStandards extends Construct { + public readonly id: string; + + constructor(scope: Construct, id: string, props: SecurityHubStandardsProps) { + super(scope, id); + + const RESOURCE_TYPE = 'Custom::SecurityHubBatchEnableStandards'; + + const provider = cdk.CustomResourceProvider.getOrCreateProvider(this, RESOURCE_TYPE, { + codeDirectory: path.join(__dirname, 'batch-enable-standards/dist'), + runtime: cdk.CustomResourceProviderRuntime.NODEJS_14_X, + policyStatements: [ + { + Sid: 'SecurityHubCreateMembersTaskSecurityHubActions', + Effect: 'Allow', + Action: [ + 'securityhub:BatchDisableStandards', + 'securityhub:BatchEnableStandards', + 'securityhub:DescribeStandards', + 'securityhub:DescribeStandardsControls', + 'securityhub:EnableSecurityHub', + 'securityhub:GetEnabledStandards', + 'securityhub:UpdateStandardsControl', + ], + Resource: '*', + }, + ], + }); + + const resource = new cdk.CustomResource(this, 'Resource', { + resourceType: RESOURCE_TYPE, + serviceToken: provider.serviceToken, + properties: { + region: cdk.Stack.of(this).region, + standards: props.standards, + }, + }); + + /** + * Singleton pattern to define the log group for the singleton function + * in the stack + */ + const stack = cdk.Stack.of(scope); + const logGroup = + (stack.node.tryFindChild(`${provider.node.id}LogGroup`) as cdk.aws_logs.LogGroup) ?? + new cdk.aws_logs.LogGroup(stack, `${provider.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${(provider.node.findChild('Handler') as cdk.aws_lambda.CfnFunction).ref}`, + retention: props.logRetentionInDays, + encryptionKey: props.kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + resource.node.addDependency(logGroup); + + this.id = resource.ref; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-servicecatalog/get-portfolio-id.ts b/source/packages/@aws-accelerator/constructs/lib/aws-servicecatalog/get-portfolio-id.ts new file mode 100644 index 000000000..627f8d8f6 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-servicecatalog/get-portfolio-id.ts @@ -0,0 +1,84 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { v4 as uuidv4 } from 'uuid'; +import { Construct } from 'constructs'; + +const path = require('path'); + +/** + * Get the PortfolioId from servicecatalog + */ +export interface GetPortfolioIdProps { + readonly displayName: string; + readonly providerName: string; + /** + * Custom resource lambda log group encryption key + */ + readonly kmsKey: cdk.aws_kms.Key; + /** + * Custom resource lambda log retention in days + */ + readonly logRetentionInDays: number; +} + +export class GetPortfolioId extends Construct { + public readonly portfolioId: string; + + constructor(scope: Construct, id: string, props: GetPortfolioIdProps) { + super(scope, id); + + const RESOURCE_TYPE = 'Custom::GetPortfolioId'; + + const provider = cdk.CustomResourceProvider.getOrCreateProvider(this, RESOURCE_TYPE, { + codeDirectory: path.join(__dirname, 'get-portfolio-id/dist'), + runtime: cdk.CustomResourceProviderRuntime.NODEJS_14_X, + policyStatements: [ + { + Sid: 'ServiceCatalog', + Effect: 'Allow', + Action: ['servicecatalog:ListPortfolios'], + Resource: '*', + }, + ], + }); + + const resource = new cdk.CustomResource(this, 'Resource', { + resourceType: RESOURCE_TYPE, + serviceToken: provider.serviceToken, + properties: { + displayName: props.displayName, + providerName: props.providerName, + uuid: uuidv4(), // Generates a new UUID to force the resource to update + }, + }); + + /** + * Singleton pattern to define the log group for the singleton function + * in the stack + */ + const stack = cdk.Stack.of(scope); + const logGroup = + (stack.node.tryFindChild(`${provider.node.id}LogGroup`) as cdk.aws_logs.LogGroup) ?? + new cdk.aws_logs.LogGroup(stack, `${provider.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${(provider.node.findChild('Handler') as cdk.aws_lambda.CfnFunction).ref}`, + retention: props.logRetentionInDays, + encryptionKey: props.kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + resource.node.addDependency(logGroup); + + this.portfolioId = resource.ref; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-servicecatalog/get-portfolio-id/index.ts b/source/packages/@aws-accelerator/constructs/lib/aws-servicecatalog/get-portfolio-id/index.ts new file mode 100644 index 000000000..cbe90e658 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-servicecatalog/get-portfolio-id/index.ts @@ -0,0 +1,68 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +/** + * get-portfolio-id - lambda handler + * + * @param event + * @returns + */ + +import * as AWS from 'aws-sdk'; +import { throttlingBackOff } from '@aws-accelerator/utils'; + +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + PhysicalResourceId: string | undefined; + Status: string; + } + | undefined +> { + const displayName = event.ResourceProperties['displayName']; + const providerName = event.ResourceProperties['providerName']; + + switch (event.RequestType) { + case 'Create': + case 'Update': + const serviceCatalogClient = new AWS.ServiceCatalog(); + let nextToken: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => + serviceCatalogClient.listPortfolios({ PageToken: nextToken }).promise(), + ); + for (const portfolio of page.PortfolioDetails ?? []) { + if (portfolio.DisplayName === displayName && portfolio.ProviderName === providerName) { + const portfolioId = portfolio.Id; + if (portfolioId) { + console.log(portfolioId); + return { + PhysicalResourceId: portfolioId, + Status: 'SUCCESS', + }; + } + } + } + nextToken = page.NextPageToken; + } while (nextToken); + + return { + PhysicalResourceId: 'none', + Status: 'SUCCESS', + }; + case 'Delete': + // Do Nothing + return { + PhysicalResourceId: event.PhysicalResourceId, + Status: 'SUCCESS', + }; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-servicecatalog/get-portfolio-id/package.json b/source/packages/@aws-accelerator/constructs/lib/aws-servicecatalog/get-portfolio-id/package.json new file mode 100644 index 000000000..de4828947 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-servicecatalog/get-portfolio-id/package.json @@ -0,0 +1,46 @@ +{ + "name": "@aws-accelerator/constructs-aws-servicecatalog-get-portfolio-id", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --outfile=./dist/index.js --platform=node --target=node14 --external:aws-sdk index.ts", + "test": "", + "testreport": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "esbuild": "0.14.2", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2" + }, + "peerDependencies": { + "eslint": "8.4.1" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1001.0" + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-servicecatalog/get-portfolio-id/tsconfig.json b/source/packages/@aws-accelerator/constructs/lib/aws-servicecatalog/get-portfolio-id/tsconfig.json new file mode 100644 index 000000000..ebcb98a79 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-servicecatalog/get-portfolio-id/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ssm/document.ts b/source/packages/@aws-accelerator/constructs/lib/aws-ssm/document.ts new file mode 100644 index 000000000..d366c4af3 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ssm/document.ts @@ -0,0 +1,99 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +const path = require('path'); + +export interface IDocument extends cdk.IResource { + readonly documentName: string; +} + +export interface DocumentProps { + readonly name: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + readonly content: any | cdk.IResolvable; + readonly documentType: string; + readonly sharedWithAccountIds: string[]; + /** + * Custom resource lambda log group encryption key + */ + readonly kmsKey: cdk.aws_kms.Key; + /** + * Custom resource lambda log retention in days + */ + readonly logRetentionInDays: number; +} + +export class Document extends cdk.Resource implements IDocument { + readonly documentName: string; + + constructor(scope: Construct, id: string, props: DocumentProps) { + super(scope, id); + + const resource = new cdk.aws_ssm.CfnDocument(this, 'Resource', { + name: props.name, + content: props.content, + documentType: props.documentType, + }); + + this.documentName = resource.ref; + + // Also need a custom resource to do the share + if (props.sharedWithAccountIds.length > 0) { + const SHARE_SSM_DOCUMENT = 'Custom::SSMShareDocument'; + + const provider = cdk.CustomResourceProvider.getOrCreateProvider(this, SHARE_SSM_DOCUMENT, { + codeDirectory: path.join(__dirname, 'share-document/dist'), + runtime: cdk.CustomResourceProviderRuntime.NODEJS_14_X, + policyStatements: [ + { + Sid: 'ShareDocumentActions', + Effect: 'Allow', + Action: ['ssm:DescribeDocumentPermission', 'ssm:ModifyDocumentPermission'], + Resource: cdk.Stack.of(this).formatArn({ + service: 'ssm', + resource: 'document', + arnFormat: cdk.ArnFormat.SLASH_RESOURCE_NAME, + resourceName: '*', + }), + }, + ], + }); + + const resource = new cdk.CustomResource(this, 'ShareDocument', { + resourceType: SHARE_SSM_DOCUMENT, + serviceToken: provider.serviceToken, + properties: { + name: this.documentName, + accountIds: props.sharedWithAccountIds, + }, + }); + + /** + * Singleton pattern to define the log group for the singleton function + * in the stack + */ + const stack = cdk.Stack.of(scope); + const logGroup = + (stack.node.tryFindChild(`${provider.node.id}LogGroup`) as cdk.aws_logs.LogGroup) ?? + new cdk.aws_logs.LogGroup(stack, `${provider.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${(provider.node.findChild('Handler') as cdk.aws_lambda.CfnFunction).ref}`, + retention: props.logRetentionInDays, + encryptionKey: props.kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + resource.node.addDependency(logGroup); + } + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ssm/get-param-value/index.ts b/source/packages/@aws-accelerator/constructs/lib/aws-ssm/get-param-value/index.ts new file mode 100644 index 000000000..44101ef76 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ssm/get-param-value/index.ts @@ -0,0 +1,79 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { throttlingBackOff } from '@aws-accelerator/utils'; +import * as AWS from 'aws-sdk'; +import * as console from 'console'; +AWS.config.logger = console; + +/** + * get ssm parameter custom control + * + * @param event + * @returns + */ +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + PhysicalResourceId: string | undefined; + Status: string; + } + | undefined +> { + const region = event.ResourceProperties['region']; + const invokingAccountID = event.ResourceProperties['invokingAccountID']; + const parameterAccountID = event.ResourceProperties['parameterAccountID']; + const assumeRoleArn = event.ResourceProperties['assumeRoleArn']; + const parameterName = event.ResourceProperties['parameterName']; + + switch (event.RequestType) { + case 'Create': + case 'Update': + let ssmClient: AWS.SSM; + if (invokingAccountID !== parameterAccountID) { + const stsClient = new AWS.STS({ region: region }); + const assumeRoleCredential = await throttlingBackOff(() => + stsClient + .assumeRole({ + RoleArn: assumeRoleArn, + RoleSessionName: 'acceleratorAssumeRoleSession', + }) + .promise(), + ); + console.log(assumeRoleCredential); + ssmClient = new AWS.SSM({ + region: region, + credentials: { + accessKeyId: assumeRoleCredential.Credentials!.AccessKeyId, + secretAccessKey: assumeRoleCredential.Credentials!.SecretAccessKey, + sessionToken: assumeRoleCredential.Credentials!.SessionToken, + expireTime: assumeRoleCredential.Credentials!.Expiration, + }, + }); + } else { + ssmClient = new AWS.SSM({ region: region }); + } + + const response = await throttlingBackOff(() => ssmClient.getParameter({ Name: parameterName }).promise()); + + console.log(response.Parameter!.Value); + + return { PhysicalResourceId: response.Parameter!.Value, Status: 'SUCCESS' }; + + case 'Delete': + // Do Nothing + return { + PhysicalResourceId: undefined, + Status: 'SUCCESS', + }; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ssm/get-param-value/package.json b/source/packages/@aws-accelerator/constructs/lib/aws-ssm/get-param-value/package.json new file mode 100644 index 000000000..7015dbfdc --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ssm/get-param-value/package.json @@ -0,0 +1,43 @@ +{ + "name": "@aws-accelerator/constructs-aws-ssm-get-param-value", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --outfile=./dist/index.js --platform=node --target=node14 --external:aws-sdk index.ts", + "test": "", + "testreport": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "esbuild": "0.14.2", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1001.0" + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ssm/get-param-value/tsconfig.json b/source/packages/@aws-accelerator/constructs/lib/aws-ssm/get-param-value/tsconfig.json new file mode 100644 index 000000000..272282f1f --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ssm/get-param-value/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} \ No newline at end of file diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ssm/put-param-value/index.ts b/source/packages/@aws-accelerator/constructs/lib/aws-ssm/put-param-value/index.ts new file mode 100644 index 000000000..91a4f015b --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ssm/put-param-value/index.ts @@ -0,0 +1,89 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as AWS from 'aws-sdk'; +import * as console from 'console'; + +import { throttlingBackOff } from '@aws-accelerator/utils'; + +AWS.config.logger = console; + +/** + * put ssm parameter custom control + * + * @param event + * @returns + */ +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + PhysicalResourceId: string | undefined; + Status: string; + } + | undefined +> { + const region = event.ResourceProperties['region']; + const invokingAccountID = event.ResourceProperties['invokingAccountID']; + const parameterAccountID = event.ResourceProperties['parameterAccountID']; + const assumeRoleArn = event.ResourceProperties['assumeRoleArn']; + const parameterName = event.ResourceProperties['parameterName']; + const parameterValue = event.ResourceProperties['parameterValue']; + + let ssmClient: AWS.SSM; + if (invokingAccountID !== parameterAccountID) { + const stsClient = new AWS.STS({ region: region }); + const assumeRoleCredential = await throttlingBackOff(() => + stsClient + .assumeRole({ + RoleArn: assumeRoleArn, + RoleSessionName: 'acceleratorAssumeRoleSession', + }) + .promise(), + ); + console.log(assumeRoleCredential); + ssmClient = new AWS.SSM({ + region: region, + credentials: { + accessKeyId: assumeRoleCredential.Credentials!.AccessKeyId, + secretAccessKey: assumeRoleCredential.Credentials!.SecretAccessKey, + sessionToken: assumeRoleCredential.Credentials!.SessionToken, + expireTime: assumeRoleCredential.Credentials!.Expiration, + }, + }); + } else { + ssmClient = new AWS.SSM({ region: region }); + } + + switch (event.RequestType) { + case 'Create': + case 'Update': + console.log(`Put SSM parameter ${parameterName}`); + + await throttlingBackOff(() => + ssmClient + .putParameter({ Name: parameterName, Value: parameterValue, Type: 'String', Overwrite: true }) + .promise(), + ); + + return { PhysicalResourceId: parameterName, Status: 'SUCCESS' }; + + case 'Delete': + console.log(`Delete SSM parameter ${event.PhysicalResourceId}`); + + await throttlingBackOff(() => ssmClient.deleteParameter({ Name: event.PhysicalResourceId }).promise()); + + return { + PhysicalResourceId: event.PhysicalResourceId, + Status: 'SUCCESS', + }; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ssm/put-param-value/package.json b/source/packages/@aws-accelerator/constructs/lib/aws-ssm/put-param-value/package.json new file mode 100644 index 000000000..328a8486c --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ssm/put-param-value/package.json @@ -0,0 +1,43 @@ +{ + "name": "@aws-accelerator/constructs-aws-ssm-put-param-value", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --outfile=./dist/index.js --platform=node --target=node14 --external:aws-sdk index.ts", + "test": "", + "testreport": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "esbuild": "0.14.2", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1001.0" + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ssm/put-param-value/tsconfig.json b/source/packages/@aws-accelerator/constructs/lib/aws-ssm/put-param-value/tsconfig.json new file mode 100644 index 000000000..272282f1f --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ssm/put-param-value/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} \ No newline at end of file diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ssm/session-manager-settings.ts b/source/packages/@aws-accelerator/constructs/lib/aws-ssm/session-manager-settings.ts new file mode 100644 index 000000000..742828081 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ssm/session-manager-settings.ts @@ -0,0 +1,275 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { RetentionDays } from 'aws-cdk-lib/aws-logs'; +import { Construct } from 'constructs'; +const path = require('path'); + +/** + * Construction properties for an S3 Bucket object. + */ +export interface SsmSessionManagerSettingsProps { + readonly s3BucketName?: string; + readonly s3KeyPrefix?: string; + readonly s3BucketKeyArn?: string; + readonly sendToS3: boolean; + readonly sendToCloudWatchLogs: boolean; + readonly cloudWatchEncryptionEnabled: boolean; + /** + * Custom resource lambda log group encryption key + */ + readonly kmsKey: cdk.aws_kms.Key; + /** + * Custom resource lambda log retention in days + */ + readonly logRetentionInDays: number; +} + +export class SsmSessionManagerSettings extends Construct { + readonly id: string; + + constructor(scope: Construct, id: string, props: SsmSessionManagerSettingsProps) { + super(scope, id); + + const sessionManagerEC2PolicyDocument = new cdk.aws_iam.PolicyDocument({ + statements: [ + new cdk.aws_iam.PolicyStatement({ + effect: cdk.aws_iam.Effect.ALLOW, + actions: [ + 'ssmmessages:CreateControlChannel', + 'ssmmessages:CreateDataChannel', + 'ssmmessages:OpenControlChannel', + 'ssmmessages:OpenDataChannel', + 'ssm:UpdateInstanceInformation', + ], + resources: ['*'], + }), + ], + }); + + let sessionManagerLogGroupName = ''; + if (props.sendToCloudWatchLogs) { + const sessionManagerLogsCmk = new cdk.aws_kms.Key(this, 'SessionManagerLogsCmk', { + enableKeyRotation: true, + description: 'AWS Accelerator Cloud Watch Logs CMK for Session Manager Logs', + alias: 'accelerator/session-manager-logging/cloud-watch-logs', + }); + + sessionManagerLogsCmk.addToResourcePolicy( + new cdk.aws_iam.PolicyStatement({ + sid: 'Enable IAM User Permissions', + principals: [new cdk.aws_iam.AccountRootPrincipal()], + actions: ['kms:*'], + resources: ['*'], + }), + ); + + sessionManagerLogsCmk.addToResourcePolicy( + new cdk.aws_iam.PolicyStatement({ + sid: 'Allow Cloud Watch Logs access', + principals: [new cdk.aws_iam.ServicePrincipal(`logs.${cdk.Stack.of(this).region}.amazonaws.com`)], + actions: ['kms:Encrypt*', 'kms:Decrypt*', 'kms:ReEncrypt*', 'kms:GenerateDataKey*', 'kms:Describe*'], + resources: ['*'], + conditions: { + ArnLike: { + 'kms:EncryptionContext:aws:logs:arn': `arn:${cdk.Stack.of(this).partition}:logs:${ + cdk.Stack.of(this).region + }:${cdk.Stack.of(this).account}:*`, + }, + }, + }), + ); + + const logGroupName = 'aws-accelerator-session-manager-logs'; + const sessionManagerLogGroup = new cdk.aws_logs.LogGroup(this, 'sessionManagerLogGroup', { + retention: RetentionDays.TEN_YEARS, + logGroupName: logGroupName, + encryptionKey: sessionManagerLogsCmk, + }); + sessionManagerLogGroupName = sessionManagerLogGroup.logGroupName; + + //Build Session Manager EC2 IAM Policy Document to allow writing to CW logs + sessionManagerEC2PolicyDocument.addStatements( + new cdk.aws_iam.PolicyStatement({ + effect: cdk.aws_iam.Effect.ALLOW, + actions: ['logs:DescribeLogGroups'], + resources: [ + `arn:${cdk.Stack.of(this).partition}:logs:${cdk.Stack.of(this).region}:${ + cdk.Stack.of(this).account + }:log-group:*`, + ], + }), + new cdk.aws_iam.PolicyStatement({ + effect: cdk.aws_iam.Effect.ALLOW, + actions: ['logs:CreateLogStream', 'logs:PutLogEvents', 'logs:DescribeLogStreams'], + resources: [ + `arn:${cdk.Stack.of(this).partition}:logs:${cdk.Stack.of(this).region}:${ + cdk.Stack.of(this).account + }:log-group:${logGroupName}:*`, + ], + }), + ); + } + + if (props.sendToS3) { + if (!props.s3BucketKeyArn || !props.s3BucketName) { + throw new Error('Bucket Key Arn and Bucket Name must be provided'); + } else { + //Build Session Manager EC2 IAM Policy Document to allow writing to S3 + sessionManagerEC2PolicyDocument.addStatements( + new cdk.aws_iam.PolicyStatement({ + effect: cdk.aws_iam.Effect.ALLOW, + actions: ['s3:PutObject', 's3:PutObjectAcl'], + resources: [ + `arn:${cdk.Stack.of(this).partition}:s3:::${props.s3BucketName}/${ + props.s3KeyPrefix ? props.s3KeyPrefix + '/*' : '*' + }`, + ], + }), + new cdk.aws_iam.PolicyStatement({ + effect: cdk.aws_iam.Effect.ALLOW, + actions: ['s3:GetEncryptionConfiguration'], + resources: [`arn:${cdk.Stack.of(this).partition}:s3:::${props.s3BucketName}`], + }), + new cdk.aws_iam.PolicyStatement({ + effect: cdk.aws_iam.Effect.ALLOW, + actions: ['kms:Decrypt', 'kms:GenerateDataKey'], + resources: [props.s3BucketKeyArn], + }), + ); + } + } + + let sessionManagerSessionCmk: cdk.aws_kms.Key | undefined = undefined; + sessionManagerSessionCmk = new cdk.aws_kms.Key(this, 'SessionManagerSessionCmk', { + enableKeyRotation: true, + description: 'AWS Accelerator Cloud Watch Logs CMK for Session Manager Logs', + alias: 'accelerator/session-manager-logging/session', + }); + + sessionManagerSessionCmk.addToResourcePolicy( + new cdk.aws_iam.PolicyStatement({ + sid: 'Enable IAM User Permissions', + principals: [new cdk.aws_iam.AccountRootPrincipal()], + actions: ['kms:*'], + resources: ['*'], + }), + ); + + sessionManagerSessionCmk.addToResourcePolicy( + new cdk.aws_iam.PolicyStatement({ + sid: 'Allow Cloud Watch Logs access', + principals: [new cdk.aws_iam.ServicePrincipal(`logs.${cdk.Stack.of(this).region}.amazonaws.com`)], + actions: ['kms:Encrypt*', 'kms:Decrypt*', 'kms:ReEncrypt*', 'kms:GenerateDataKey*', 'kms:Describe*'], + resources: ['*'], + conditions: { + ArnLike: { + 'kms:EncryptionContext:aws:logs:arn': `arn:${cdk.Stack.of(this).partition}:logs:${ + cdk.Stack.of(this).region + }:${cdk.Stack.of(this).account}:*`, + }, + }, + }), + ); + + //Build Session Manager EC2 IAM Policy Document to allow kms action for session key + sessionManagerEC2PolicyDocument.addStatements( + new cdk.aws_iam.PolicyStatement({ + effect: cdk.aws_iam.Effect.ALLOW, + actions: ['kms:Decrypt'], + resources: [sessionManagerSessionCmk.keyArn], + }), + ); + + const sessionManagerEC2Policy = new cdk.aws_iam.ManagedPolicy(this, 'SessionManagerEC2Policy', { + document: sessionManagerEC2PolicyDocument, + managedPolicyName: `SessionManagerLogging-${cdk.Stack.of(this).region}`, + }); + + //Create an EC2 role that can be used for Session Manager + const sessionManagerEC2Role = new cdk.aws_iam.Role(this, 'SessionManagerEC2Role', { + assumedBy: new cdk.aws_iam.ServicePrincipal('ec2.amazonaws.com'), + description: 'IAM Role for an EC2 configured for Session Manager Logging', + managedPolicies: [sessionManagerEC2Policy], + roleName: `SessionManagerEC2Role-${cdk.Stack.of(this).region}`, + }); + + //Create an EC2 instance profile + new cdk.aws_iam.CfnInstanceProfile(this, 'SessionManagerEC2InstanceProfile', { + roles: [sessionManagerEC2Role.roleName], + instanceProfileName: `SessionManagerEc2Role-${cdk.Stack.of(this).region}`, + }); + + const sessionManagerUserPolicyDocument = new cdk.aws_iam.PolicyDocument({ + statements: [ + new cdk.aws_iam.PolicyStatement({ + effect: cdk.aws_iam.Effect.ALLOW, + actions: ['kms:Decrypt', 'kms:GenerateDataKey'], + resources: [sessionManagerSessionCmk.keyArn], + }), + ], + }); + + //Create an IAM Policy for users to be able to use Session Manager with KMS encryption + new cdk.aws_iam.ManagedPolicy(this, 'SessionManagerUserKMSPolicy', { + document: sessionManagerUserPolicyDocument, + managedPolicyName: `SessionManagerUserKMSPolicy-${cdk.Stack.of(this).region}`, + }); + + // + // Function definition for the custom resource + // + const provider = cdk.CustomResourceProvider.getOrCreateProvider(this, 'Custom::SessionManagerLogging', { + codeDirectory: path.join(__dirname, 'session-manager-settings/dist'), + runtime: cdk.CustomResourceProviderRuntime.NODEJS_14_X, + policyStatements: [ + { + Effect: 'Allow', + Action: ['ssm:DescribeDocument', 'ssm:CreateDocument', 'ssm:UpdateDocument'], + Resource: '*', + }, + ], + }); + + const resource = new cdk.CustomResource(this, 'Resource', { + resourceType: 'Custom::SsmSessionManagerSettings', + serviceToken: provider.serviceToken, + properties: { + s3BucketName: props.s3BucketName, + s3KeyPrefix: props.s3KeyPrefix, + s3EncryptionEnabled: props.sendToS3, //set to true if sending to S3 + cloudWatchLogGroupName: sessionManagerLogGroupName, + cloudWatchEncryptionEnabled: props.cloudWatchEncryptionEnabled, + kmsKeyId: sessionManagerSessionCmk.keyId, + }, + }); + + /** + * Singleton pattern to define the log group for the singleton function + * in the stack + */ + const stack = cdk.Stack.of(scope); + const logGroup = + (stack.node.tryFindChild(`${provider.node.id}LogGroup`) as cdk.aws_logs.LogGroup) ?? + new cdk.aws_logs.LogGroup(stack, `${provider.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${(provider.node.findChild('Handler') as cdk.aws_lambda.CfnFunction).ref}`, + retention: props.logRetentionInDays, + encryptionKey: props.kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + resource.node.addDependency(logGroup); + + this.id = resource.ref; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ssm/session-manager-settings/index.ts b/source/packages/@aws-accelerator/constructs/lib/aws-ssm/session-manager-settings/index.ts new file mode 100644 index 000000000..2fb176fa9 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ssm/session-manager-settings/index.ts @@ -0,0 +1,100 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { throttlingBackOff } from '@aws-accelerator/utils'; +import * as AWS from 'aws-sdk'; +import { CreateDocumentRequest, UpdateDocumentRequest } from 'aws-sdk/clients/ssm'; +AWS.config.logger = console; + +const documentName = 'SSM-SessionManagerRunShell'; +const ssm = new AWS.SSM(); + +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + PhysicalResourceId: string | undefined; + Status: string; + } + | undefined +> { + const s3BucketName: string = event.ResourceProperties['s3BucketName']; + const s3KeyPrefix: string = event.ResourceProperties['s3KeyPrefix']; + const s3EncryptionEnabled: boolean = event.ResourceProperties['s3EncryptionEnabled'] === 'true'; + const cloudWatchLogGroupName: string = event.ResourceProperties['cloudWatchLogGroupName']; + const cloudWatchEncryptionEnabled: boolean = event.ResourceProperties['cloudWatchEncryptionEnabled'] === 'true'; + const kmsKeyId: string = event.ResourceProperties['kmsKeyId']; + switch (event.RequestType) { + case 'Create': + case 'Update': + // Based on doc: https://docs.aws.amazon.com/systems-manager/latest/userguide/getting-started-configure-preferences-cli.html + const settings = { + schemaVersion: '1.0', + description: 'Document to hold regional settings for Session Manager', + sessionType: 'Standard_Stream', + inputs: { + cloudWatchEncryptionEnabled, + cloudWatchLogGroupName, + kmsKeyId, + s3BucketName, + s3EncryptionEnabled, + s3KeyPrefix: s3KeyPrefix ?? '', + runAsEnabled: false, + runAsDefaultUser: '', + }, + }; + try { + await throttlingBackOff(() => + ssm + .describeDocument({ + Name: documentName, + }) + .promise(), + ); + const updateDocumentRequest: UpdateDocumentRequest = { + Content: JSON.stringify(settings), + Name: documentName, + DocumentVersion: '$LATEST', + }; + console.log('Update SSM Document Request: ', updateDocumentRequest); + await throttlingBackOff(() => ssm.updateDocument(updateDocumentRequest).promise()); + console.log('Update SSM Document Success'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (error: any) { + if (error.code === 'DuplicateDocumentContent') { + console.log(`SSM Document is Already latest :${documentName}`); + } else if (error.code === 'InvalidDocument') { + const createDocumentRequest: CreateDocumentRequest = { + Content: JSON.stringify(settings), + Name: documentName, + DocumentType: `Session`, + }; + console.log('Create SSM Document Request: ', createDocumentRequest); + await throttlingBackOff(() => ssm.createDocument(createDocumentRequest).promise()); + console.log('Create SSM Document Success'); + } else { + throw error; + } + } + + return { + PhysicalResourceId: 'session-manager-settings', + Status: 'SUCCESS', + }; + + case 'Delete': + // Do Nothing + return { + PhysicalResourceId: event.PhysicalResourceId, + Status: 'SUCCESS', + }; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ssm/session-manager-settings/package.json b/source/packages/@aws-accelerator/constructs/lib/aws-ssm/session-manager-settings/package.json new file mode 100644 index 000000000..010a94ed0 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ssm/session-manager-settings/package.json @@ -0,0 +1,43 @@ +{ + "name": "@aws-accelerator/constructs-aws-ssem-session-manager-settings", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "esbuild --minify --bundle --outfile=./dist/index.js --platform=node --target=node14 --external:aws-sdk index.ts", + "test": "", + "testreport": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "esbuild": "0.14.2", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.944.0" + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ssm/session-manager-settings/tsconfig.json b/source/packages/@aws-accelerator/constructs/lib/aws-ssm/session-manager-settings/tsconfig.json new file mode 100644 index 000000000..ebcb98a79 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ssm/session-manager-settings/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ssm/share-document/index.ts b/source/packages/@aws-accelerator/constructs/lib/aws-ssm/share-document/index.ts new file mode 100644 index 000000000..7bea27476 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ssm/share-document/index.ts @@ -0,0 +1,101 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { throttlingBackOff } from '@aws-accelerator/utils'; +import * as AWS from 'aws-sdk'; +AWS.config.logger = console; + +/** + * share-document - lambda handler + * + * @param event + * @returns + */ +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise< + | { + PhysicalResourceId: string | undefined; + Status: string; + } + | undefined +> { + const name = event.ResourceProperties['name']; + const accountIds: string[] = event.ResourceProperties['accountIds']; + const ssmClient = new AWS.SSM({}); + + const documentPermission = await throttlingBackOff(() => + ssmClient.describeDocumentPermission({ Name: name, PermissionType: 'Share' }).promise(), + ); + console.log('DescribeDocumentPermissionCommand:'); + console.log(JSON.stringify(documentPermission)); + + switch (event.RequestType) { + case 'Create': + case 'Update': + const accountIdsToAdd: string[] = []; + const accountIdsToRemove: string[] = []; + + // Identify accounts to add + accountIds.forEach(accountId => { + if (!documentPermission.AccountIds?.includes(accountId)) { + accountIdsToAdd.push(accountId); + } + }); + + // Identify accounts to remove + documentPermission.AccountIds?.forEach(accountId => { + if (!accountIds.includes(accountId)) { + accountIdsToRemove.push(accountId); + } + }); + + console.log(`accountIdsToAdd: ${accountIdsToAdd}`); + console.log(`accountIdsToRemove: ${accountIdsToRemove}`); + + const response = await throttlingBackOff(() => + ssmClient + .modifyDocumentPermission({ + Name: name, + PermissionType: 'Share', + AccountIdsToAdd: accountIdsToAdd, + AccountIdsToRemove: accountIdsToRemove, + }) + .promise(), + ); + console.log('ModifyDocumentPermissionCommand:'); + console.log(JSON.stringify(response)); + + return { + PhysicalResourceId: 'share-document', + Status: 'SUCCESS', + }; + + case 'Delete': + console.log('Start un-sharing the document'); + console.log('Following accounts to be un-share'); + console.log(documentPermission.AccountIds); + // Remove sharing + await throttlingBackOff(() => + ssmClient + .modifyDocumentPermission({ + Name: name, + PermissionType: 'Share', + AccountIdsToRemove: documentPermission.AccountIds, + }) + .promise(), + ); + return { + PhysicalResourceId: event.PhysicalResourceId, + Status: 'SUCCESS', + }; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ssm/share-document/package.json b/source/packages/@aws-accelerator/constructs/lib/aws-ssm/share-document/package.json new file mode 100644 index 000000000..ab82db115 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ssm/share-document/package.json @@ -0,0 +1,44 @@ +{ + "name": "@aws-accelerator/constructs-aws-ssm-share-document", + "version": "0.0.0", + "private": true, + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "esbuild --minify --bundle --outfile=./dist/index.js --platform=node --target=node14 --external:aws-sdk index.ts", + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "lint": "eslint --fix --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "test": "", + "testreport": "" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1101.0" + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "esbuild": "0.14.2", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-node": "10.7.0", + "ts-jest": "27.1.1", + "typescript": "4.5.2" + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ssm/share-document/tsconfig.json b/source/packages/@aws-accelerator/constructs/lib/aws-ssm/share-document/tsconfig.json new file mode 100644 index 000000000..ebcb98a79 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ssm/share-document/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["test/**/*"] +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ssm/ssm-parameter-lookup.ts b/source/packages/@aws-accelerator/constructs/lib/aws-ssm/ssm-parameter-lookup.ts new file mode 100644 index 000000000..77d165d87 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ssm/ssm-parameter-lookup.ts @@ -0,0 +1,108 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import { v4 as uuidv4 } from 'uuid'; + +const path = require('path'); + +/** + * SsmParameterLookupProps + */ +export interface SsmParameterLookupProps { + /** + * Name of the parameter + */ + readonly name: string; + /** + * Parameter account id + */ + readonly accountId: string; + /** + * The name of the cross account role to use when accessing + */ + readonly roleName: string; + /** + * Custom resource lambda log group encryption key + */ + readonly kmsKey?: cdk.aws_kms.Key; + /** + * Custom resource lambda log retention in days + */ + readonly logRetentionInDays?: number; +} + +/** + * SsmParameterLookup class - to get ssm parameter value from other account + */ +export class SsmParameterLookup extends Construct { + public readonly value: string = ''; + + constructor(scope: Construct, id: string, props: SsmParameterLookupProps) { + super(scope, id); + + const RESOURCE_TYPE = 'Custom::SsmGetParameterValue'; + + const roleArn = `arn:${cdk.Stack.of(this).partition}:iam::${props.accountId}:role/${props.roleName}`; + + const provider = cdk.CustomResourceProvider.getOrCreateProvider(this, RESOURCE_TYPE, { + codeDirectory: path.join(__dirname, 'get-param-value/dist'), + runtime: cdk.CustomResourceProviderRuntime.NODEJS_14_X, + policyStatements: [ + { + Sid: 'SsmGetParameterActions', + Effect: cdk.aws_iam.Effect.ALLOW, + Action: ['ssm:GetParameters', 'ssm:GetParameter', 'ssm:DescribeParameters'], + Resource: ['*'], + }, + { + Sid: 'StsAssumeRoleActions', + Effect: cdk.aws_iam.Effect.ALLOW, + Action: ['sts:AssumeRole'], + Resource: [roleArn], + }, + ], + }); + + const resource = new cdk.CustomResource(this, 'Resource', { + resourceType: RESOURCE_TYPE, + serviceToken: provider.serviceToken, + properties: { + region: cdk.Stack.of(this).region, + parameterAccountID: props.accountId, + parameterName: props.name, + assumeRoleArn: roleArn, + invokingAccountID: cdk.Stack.of(this).account, + uuid: uuidv4(), + }, + }); + + /** + * Singleton pattern to define the log group for the singleton function + * in the stack + */ + const stack = cdk.Stack.of(scope); + const logGroup = + (stack.node.tryFindChild(`${provider.node.id}LogGroup`) as cdk.aws_logs.LogGroup) ?? + new cdk.aws_logs.LogGroup(stack, `${provider.node.id}LogGroup`, { + logGroupName: `/aws/lambda/${(provider.node.findChild('Handler') as cdk.aws_lambda.CfnFunction).ref}`, + retention: props.logRetentionInDays, + encryptionKey: props.kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + resource.node.addDependency(logGroup); + + this.value = resource.ref; + } +} diff --git a/source/packages/@aws-accelerator/constructs/lib/aws-ssm/ssm-parameter.ts b/source/packages/@aws-accelerator/constructs/lib/aws-ssm/ssm-parameter.ts new file mode 100644 index 000000000..56dc6fdd6 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/lib/aws-ssm/ssm-parameter.ts @@ -0,0 +1,159 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import * as path from 'path'; + +export enum SsmParameterType { + GET = 'GET', + PUT = 'PUT', +} +/** + * SsmParameterProps + */ +export interface SsmParameterProps { + readonly region: string; + readonly partition: string; + /** + * SSM Parameter + */ + readonly parameter: { + /** + * Name of the parameter + */ + name: string; + /** + * Target account id of the parameter + */ + accountId: string; + /** + * Role name to assume to access the parameter in target account + */ + roleName: string; + /** + * Optional value to put when when using SsmParameterType.PUT + */ + value?: string; + }; + readonly invokingAccountID: string; + + /** + * The type of SSM parameter operation + */ + readonly type: SsmParameterType; +} + +/** + * SsmParameter class - to get ssm parameter value from other account + */ +export class SsmParameter extends Construct { + public readonly parameterName: string; + public readonly value: string = ''; + + constructor(scope: Construct, id: string, props: SsmParameterProps) { + super(scope, id); + + this.parameterName = props.parameter.name; + const assumeRoleArn = `arn:${props.partition}:iam::${props.parameter.accountId}:role/${props.parameter.roleName}`; + + let RESOURCE_TYPE: string; + let codeDir: string; + let desc: string; + let logicalId: string; + const policyStatements: cdk.aws_iam.PolicyStatement[] = []; + + if (props.type === SsmParameterType.GET) { + RESOURCE_TYPE = 'Custom::SsmGetParameterValue'; + codeDir = 'get-param-value/dist'; + desc = `Custom resource provider to get ssm parameter ${props.parameter.name} value`; + logicalId = 'SsmGetParameterValueFunction'; + + // Push policy statement + policyStatements.push( + new cdk.aws_iam.PolicyStatement({ + sid: 'SsmGetParameterActions', + effect: cdk.aws_iam.Effect.ALLOW, + actions: ['ssm:GetParameters', 'ssm:GetParameter', 'ssm:DescribeParameters'], + resources: ['*'], + }), + ); + } else if (props.type === SsmParameterType.PUT) { + // Check if parameter value is included in props + if (!props.parameter.value) { + throw new Error('parameter.value property required when type is set to PUT'); + } + + RESOURCE_TYPE = 'Custom::SsmPutParameterValue'; + codeDir = 'put-param-value/dist'; + desc = `Custom resource provider to put ssm parameter ${props.parameter.name} value`; + logicalId = 'SsmPutParameterValueFunction'; + + // Push policy statement + policyStatements.push( + new cdk.aws_iam.PolicyStatement({ + sid: 'SsmPutParameterActions', + effect: cdk.aws_iam.Effect.ALLOW, + actions: ['ssm:DeleteParameter', 'ssm:PutParameter'], + resources: ['*'], + }), + ); + + this.value = props.parameter.value; + } else { + throw new Error(`SSM parameter type ${props.type} is invalid`); + } + + policyStatements.push( + new cdk.aws_iam.PolicyStatement({ + sid: 'StsAssumeRoleActions', + effect: cdk.aws_iam.Effect.ALLOW, + actions: ['sts:AssumeRole'], + resources: [assumeRoleArn], + }), + ); + + // Create custom resource + const customResourceLambdaFunction = new cdk.aws_lambda.Function(this, logicalId, { + code: cdk.aws_lambda.Code.fromAsset(path.join(__dirname, codeDir)), + runtime: cdk.aws_lambda.Runtime.NODEJS_14_X, + handler: 'index.handler', + description: desc, + }); + + policyStatements.forEach(policyStatement => { + customResourceLambdaFunction?.addToRolePolicy(policyStatement); + }); + + const customResourceProvider = new cdk.custom_resources.Provider(this, 'CustomResourceProvider', { + onEventHandler: customResourceLambdaFunction, + }); + + const resource = new cdk.CustomResource(this, 'Resource', { + resourceType: RESOURCE_TYPE, + serviceToken: customResourceProvider.serviceToken, + properties: { + region: props.region, + parameterAccountID: props.parameter.accountId, + parameterName: props.parameter.name, + parameterValue: props.parameter.value, + assumeRoleArn: assumeRoleArn, + invokingAccountID: props.invokingAccountID, + }, + }); + + if (props.type === SsmParameterType.GET) { + this.value = resource.ref; + } + } +} diff --git a/source/packages/@aws-accelerator/constructs/package.json b/source/packages/@aws-accelerator/constructs/package.json new file mode 100644 index 000000000..6cc5e4647 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/package.json @@ -0,0 +1,49 @@ +{ + "name": "@aws-accelerator/constructs", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "main": "index.js", + "types": "index.d.ts", + "private": true, + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "tsc", + "watch": "tsc -w", + "test": "jest --coverage --ci", + "lint": "eslint --fix --max-warnings 0 -c ../../../.eslintrc.json 'lib/**/*.{ts,tsx}' 'test/**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../.eslintrc.json 'lib/**/*.{ts,tsx}' 'test/**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "aws-cdk-lib": "2.16.0", + "constructs": "10.0.12", + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2" + }, + "peerDependencies": { + "aws-cdk-lib": "2.16.0", + "es-lint": "8.4.1" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-cdk-lib": "2.16.0", + "@types/uuid": "8.3.3" + } +} diff --git a/source/packages/@aws-accelerator/constructs/test/aws-cur/report-definition.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-cur/report-definition.test.ts new file mode 100644 index 000000000..c0eca720c --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-cur/report-definition.test.ts @@ -0,0 +1,349 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; + +import { ReportDefinition } from '../../lib/aws-cur/report-definition'; + +const testNamePrefix = 'Construct(ReportDefinition): '; + +const app = new cdk.App(); + +// Create stack for native Cfn construct +const nativeEnv = { account: '333333333333', region: 'us-east-1' }; +const nativeStack = new cdk.Stack(app, 'NativeStack', { env: nativeEnv }); +const nativeBucket = new cdk.aws_s3.Bucket(nativeStack, 'TestBucket'); +const nativeKey = new cdk.aws_kms.Key(nativeStack, 'NativeStack', {}); + +// Create stack for custom Cfn construct +const customEnv = { account: '333333333333', region: 'us-west-1' }; +const customStack = new cdk.Stack(app, 'CustomStack', { env: customEnv }); +const customBucket = new cdk.aws_s3.Bucket(customStack, 'TestBucket'); +const customKey = new cdk.aws_kms.Key(customStack, 'CustomKey', {}); + +// Create report definitions for each stack +new ReportDefinition(nativeStack, 'TestReportDefinition', { + compression: 'Parquet', + format: 'Parquet', + refreshClosedReports: true, + reportName: 'Test', + reportVersioning: 'OVERWRITE_REPORT', + s3Bucket: nativeBucket, + s3Prefix: 'test', + s3Region: cdk.Stack.of(nativeStack).region, + timeUnit: 'DAILY', + kmsKey: nativeKey, + logRetentionInDays: 3653, +}); + +new ReportDefinition(customStack, 'TestReportDefinition', { + compression: 'Parquet', + format: 'Parquet', + refreshClosedReports: true, + reportName: 'Test', + reportVersioning: 'OVERWRITE_REPORT', + s3Bucket: customBucket, + s3Prefix: 'test', + s3Region: cdk.Stack.of(customStack).region, + timeUnit: 'DAILY', + kmsKey: customKey, + logRetentionInDays: 3653, +}); + +/** + * Report Definition construct test + */ +describe('ReportDefinition', () => { + /** + * Native report definition resource count tets + */ + test(`${testNamePrefix} Native report definition resource count test`, () => { + cdk.assertions.Template.fromStack(nativeStack).resourceCountIs('AWS::CUR::ReportDefinition', 1); + }); + + /** + * Native bucket policy resource count test + */ + test(`${testNamePrefix} Native bucket policy resource count test`, () => { + cdk.assertions.Template.fromStack(nativeStack).resourceCountIs('AWS::S3::BucketPolicy', 1); + }); + + /** + * Custom report definition resource count tets + */ + test(`${testNamePrefix} Custom report definition resource count test`, () => { + cdk.assertions.Template.fromStack(customStack).resourceCountIs('Custom::CrossRegionReportDefinition', 1); + }); + + /** + * Custom bucket policy resource count test + */ + test(`${testNamePrefix} Custom bucket policy resource count test`, () => { + cdk.assertions.Template.fromStack(customStack).resourceCountIs('AWS::S3::BucketPolicy', 1); + }); + + /** + * Custom IAM role resource count test + */ + test(`${testNamePrefix} Custom IAM role resource count test`, () => { + cdk.assertions.Template.fromStack(customStack).resourceCountIs('AWS::IAM::Role', 1); + }); + + /** + * Custom Lambda function resource count test + */ + test(`${testNamePrefix} Custom Lambda function resource count test`, () => { + cdk.assertions.Template.fromStack(customStack).resourceCountIs('AWS::Lambda::Function', 1); + }); + + /** + * Native report definition resource configuration test + */ + test(`${testNamePrefix} Native report definition resource configuration test`, () => { + cdk.assertions.Template.fromStack(nativeStack).templateMatches({ + Resources: { + TestReportDefinition9701AAC4: { + Type: 'AWS::CUR::ReportDefinition', + DependsOn: ['TestBucketPolicyBA12ED38'], + Properties: { + Compression: 'Parquet', + Format: 'Parquet', + RefreshClosedReports: true, + ReportName: 'Test', + ReportVersioning: 'OVERWRITE_REPORT', + S3Bucket: { Ref: 'TestBucket560B80BC' }, + S3Prefix: 'test', + S3Region: 'us-east-1', + TimeUnit: 'DAILY', + }, + }, + }, + }); + }); + + /** + * Native bucket policy resource configuration test + */ + test(`${testNamePrefix} Native bucket policy resource configuration test`, () => { + cdk.assertions.Template.fromStack(nativeStack).templateMatches({ + Resources: { + TestBucketPolicyBA12ED38: { + Type: 'AWS::S3::BucketPolicy', + Properties: { + Bucket: { Ref: 'TestBucket560B80BC' }, + PolicyDocument: { + Statement: [ + { + Action: ['s3:GetBucketAcl', 's3:GetBucketPolicy'], + Effect: 'Allow', + Principal: { + Service: 'billingreports.amazonaws.com', + }, + Resource: { + 'Fn::GetAtt': ['TestBucket560B80BC', 'Arn'], + }, + Condition: { + StringEquals: { + 'aws:SourceAccount': { Ref: 'AWS::AccountId' }, + 'aws:SourceArn': { + 'Fn::Join': ['', ['arn:aws:cur:us-east-1:', { Ref: 'AWS::AccountId' }, ':definition/*']], + }, + }, + }, + }, + { + Action: 's3:PutObject', + Effect: 'Allow', + Principal: { + Service: 'billingreports.amazonaws.com', + }, + Resource: { + 'Fn::Join': ['', [{ 'Fn::GetAtt': ['TestBucket560B80BC', 'Arn'] }, '/*']], + }, + Condition: { + StringEquals: { + 'aws:SourceAccount': { Ref: 'AWS::AccountId' }, + 'aws:SourceArn': { + 'Fn::Join': ['', ['arn:aws:cur:us-east-1:', { Ref: 'AWS::AccountId' }, ':definition/*']], + }, + }, + }, + }, + ], + }, + }, + }, + }, + }); + }); + + /** + * Custom report definition resource configuration test + */ + test(`${testNamePrefix} Custom report definition resource configuration test`, () => { + cdk.assertions.Template.fromStack(customStack).templateMatches({ + Resources: { + TestReportDefinition9701AAC4: { + Type: 'Custom::CrossRegionReportDefinition', + DependsOn: [ + 'CustomCrossRegionReportDefinitionCustomResourceProviderLogGroupC64E88D7', + 'TestBucketPolicyBA12ED38', + ], + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomCrossRegionReportDefinitionCustomResourceProviderHandler8E3AEE17', 'Arn'], + }, + reportDefinition: { + AdditionalSchemaElements: [], + Compression: 'Parquet', + Format: 'Parquet', + RefreshClosedReports: true, + ReportName: 'Test', + ReportVersioning: 'OVERWRITE_REPORT', + S3Bucket: { Ref: 'TestBucket560B80BC' }, + S3Prefix: 'test', + S3Region: 'us-west-1', + TimeUnit: 'DAILY', + }, + }, + }, + }, + }); + }); + + /** + * Custom bucket policy resource configuration test + */ + test(`${testNamePrefix} Custom bucket policy resource configuration test`, () => { + cdk.assertions.Template.fromStack(customStack).templateMatches({ + Resources: { + TestBucketPolicyBA12ED38: { + Type: 'AWS::S3::BucketPolicy', + Properties: { + Bucket: { Ref: 'TestBucket560B80BC' }, + PolicyDocument: { + Statement: [ + { + Action: ['s3:GetBucketAcl', 's3:GetBucketPolicy'], + Effect: 'Allow', + Principal: { + Service: 'billingreports.amazonaws.com', + }, + Resource: { + 'Fn::GetAtt': ['TestBucket560B80BC', 'Arn'], + }, + Condition: { + StringEquals: { + 'aws:SourceAccount': { Ref: 'AWS::AccountId' }, + 'aws:SourceArn': { + 'Fn::Join': ['', ['arn:aws:cur:us-east-1:', { Ref: 'AWS::AccountId' }, ':definition/*']], + }, + }, + }, + }, + { + Action: 's3:PutObject', + Effect: 'Allow', + Principal: { + Service: 'billingreports.amazonaws.com', + }, + Resource: { + 'Fn::Join': ['', [{ 'Fn::GetAtt': ['TestBucket560B80BC', 'Arn'] }, '/*']], + }, + Condition: { + StringEquals: { + 'aws:SourceAccount': { Ref: 'AWS::AccountId' }, + 'aws:SourceArn': { + 'Fn::Join': ['', ['arn:aws:cur:us-east-1:', { Ref: 'AWS::AccountId' }, ':definition/*']], + }, + }, + }, + }, + ], + }, + }, + }, + }, + }); + }); + + /** + * Custom IAM role resource configuration test + */ + test(`${testNamePrefix} Custom IAM role resource configuration test`, () => { + cdk.assertions.Template.fromStack(customStack).templateMatches({ + Resources: { + CustomCrossRegionReportDefinitionCustomResourceProviderRole845A4C3A: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: ['cur:DeleteReportDefinition', 'cur:ModifyReportDefinition', 'cur:PutReportDefinition'], + Effect: 'Allow', + Resource: '*', + }, + ], + }, + }, + ], + }, + }, + }, + }); + }); + + /** + * Custom Lambda function resource configuration test + */ + test(`${testNamePrefix} Custom Lambda function resource configuration test`, () => { + cdk.assertions.Template.fromStack(customStack).templateMatches({ + Resources: { + CustomCrossRegionReportDefinitionCustomResourceProviderHandler8E3AEE17: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'cdk-hnb659fds-assets-333333333333-us-west-1', + S3Key: cdk.assertions.Match.stringLikeRegexp('\\w+.zip'), + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomCrossRegionReportDefinitionCustomResourceProviderRole845A4C3A', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-ec2/__snapshots__/dhcp-options.test.ts.snap b/source/packages/@aws-accelerator/constructs/test/aws-ec2/__snapshots__/dhcp-options.test.ts.snap new file mode 100644 index 000000000..a7fd54787 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-ec2/__snapshots__/dhcp-options.test.ts.snap @@ -0,0 +1,30 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DhcpOptions Construct(DhcpOptions): Snapshot Test 1`] = ` +Object { + "Resources": Object { + "TestDhcpOpts22CADF8A": Object { + "Properties": Object { + "DomainName": "test.com", + "DomainNameServers": Array [ + "1.1.1.1", + ], + "NetbiosNameServers": Array [ + "1.1.1.1", + ], + "NetbiosNodeType": 2, + "NtpServers": Array [ + "1.1.1.1", + ], + "Tags": Array [ + Object { + "Key": "Name", + "Value": "Test", + }, + ], + }, + "Type": "AWS::EC2::DHCPOptions", + }, + }, +} +`; diff --git a/source/packages/@aws-accelerator/constructs/test/aws-ec2/__snapshots__/prefix-list.test.ts.snap b/source/packages/@aws-accelerator/constructs/test/aws-ec2/__snapshots__/prefix-list.test.ts.snap new file mode 100644 index 000000000..cbd1aed50 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-ec2/__snapshots__/prefix-list.test.ts.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PrefixList Construct(PrefixList): Snapshot Test 1`] = ` +Object { + "Resources": Object { + "TestPrefixListF3A076C9": Object { + "Properties": Object { + "AddressFamily": "IPv4", + "Entries": Array [ + Object { + "Cidr": "1.1.1.1/32", + }, + ], + "MaxEntries": 1, + "PrefixListName": "Test", + "Tags": Array [ + Object { + "Key": "Test-Key", + "Value": "Test-Value", + }, + ], + }, + "Type": "AWS::EC2::PrefixList", + }, + }, +} +`; diff --git a/source/packages/@aws-accelerator/constructs/test/aws-ec2/__snapshots__/route-table.test.ts.snap b/source/packages/@aws-accelerator/constructs/test/aws-ec2/__snapshots__/route-table.test.ts.snap new file mode 100644 index 000000000..0957dfaee --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-ec2/__snapshots__/route-table.test.ts.snap @@ -0,0 +1,63 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RouteTable Construct(RouteTable): Snapshot Test 1`] = ` +Object { + "Resources": Object { + "RouteTable82FB8FA6": Object { + "Properties": Object { + "Tags": Array [ + Object { + "Key": "Name", + "Value": "TestRouteTable", + }, + Object { + "Key": "Test-Key", + "Value": "Test-Value", + }, + ], + "VpcId": Object { + "Ref": "TestVpcE77CE678", + }, + }, + "Type": "AWS::EC2::RouteTable", + }, + "TestVpcE77CE678": Object { + "Properties": Object { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": false, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": Array [ + Object { + "Key": "Name", + "Value": "Test", + }, + ], + }, + "Type": "AWS::EC2::VPC", + }, + "TestVpcInternetGateway01360C82": Object { + "Properties": Object { + "Tags": Array [ + Object { + "Key": "Name", + "Value": "Test", + }, + ], + }, + "Type": "AWS::EC2::InternetGateway", + }, + "TestVpcInternetGatewayAttachment60E451D5": Object { + "Properties": Object { + "InternetGatewayId": Object { + "Ref": "TestVpcInternetGateway01360C82", + }, + "VpcId": Object { + "Ref": "TestVpcE77CE678", + }, + }, + "Type": "AWS::EC2::VPCGatewayAttachment", + }, + }, +} +`; diff --git a/source/packages/@aws-accelerator/constructs/test/aws-ec2/__snapshots__/transit-gateway-route-table.test.ts.snap b/source/packages/@aws-accelerator/constructs/test/aws-ec2/__snapshots__/transit-gateway-route-table.test.ts.snap new file mode 100644 index 000000000..1e93a84fd --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-ec2/__snapshots__/transit-gateway-route-table.test.ts.snap @@ -0,0 +1,24 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TransitGatewayRouteTable Construct(TransitGatewayRouteTable): Snapshot Test 1`] = ` +Object { + "Resources": Object { + "TransitGatewayRouteTableCoreTransitGatewayRouteTableD6BC94E0": Object { + "Properties": Object { + "Tags": Array [ + Object { + "Key": "Name", + "Value": "core", + }, + Object { + "Key": "Test-Key", + "Value": "Test-Value", + }, + ], + "TransitGatewayId": "tgw0001", + }, + "Type": "AWS::EC2::TransitGatewayRouteTable", + }, + }, +} +`; diff --git a/source/packages/@aws-accelerator/constructs/test/aws-ec2/__snapshots__/transit-gateway.test.ts.snap b/source/packages/@aws-accelerator/constructs/test/aws-ec2/__snapshots__/transit-gateway.test.ts.snap new file mode 100644 index 000000000..ae73ada87 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-ec2/__snapshots__/transit-gateway.test.ts.snap @@ -0,0 +1,15 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TransitGatewayRouteTableAssociation Construct(TransitGatewayRouteTableAssociation): Snapshot Test 1`] = ` +Object { + "Resources": Object { + "TransitGatewayRouteTableAssociation19E386E4": Object { + "Properties": Object { + "TransitGatewayAttachmentId": "transitGatewayAttachmentId", + "TransitGatewayRouteTableId": "transitGatewayRouteTableId", + }, + "Type": "AWS::EC2::TransitGatewayRouteTableAssociation", + }, + }, +} +`; diff --git a/source/packages/@aws-accelerator/constructs/test/aws-ec2/__snapshots__/vpc-endpoint.test.ts.snap b/source/packages/@aws-accelerator/constructs/test/aws-ec2/__snapshots__/vpc-endpoint.test.ts.snap new file mode 100644 index 000000000..14a932424 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-ec2/__snapshots__/vpc-endpoint.test.ts.snap @@ -0,0 +1,65 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`VpcEndpoint Construct(VpcEndpoint): Snapshot Test 1`] = ` +Object { + "Resources": Object { + "TestSecurityGroupDA4B5F83": Object { + "Properties": Object { + "GroupDescription": "AWS Private Endpoint Zone", + "GroupName": "TestSecurityGroup", + "Tags": Array [ + Object { + "Key": "Name", + "Value": "TestSecurityGroup", + }, + ], + "VpcId": "Test", + }, + "Type": "AWS::EC2::SecurityGroup", + }, + "VpcEndpoint80208C18": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "*", + "Condition": Object { + "StringEquals": Object { + "aws:PrincipalOrgID": Array [ + "organizationId", + ], + }, + }, + "Effect": "Allow", + "Principal": Object { + "AWS": "*", + }, + "Resource": "*", + "Sid": "AccessToTrustedPrincipalsAndResources", + }, + ], + "Version": "2012-10-17", + }, + "RouteTableIds": Array [ + "Test1", + "Test2", + ], + "ServiceName": Object { + "Fn::Join": Array [ + "", + Array [ + "com.amazonaws.", + Object { + "Ref": "AWS::Region", + }, + ".service", + ], + ], + }, + "VpcId": "Test", + }, + "Type": "AWS::EC2::VPCEndpoint", + }, + }, +} +`; diff --git a/source/packages/@aws-accelerator/constructs/test/aws-ec2/__snapshots__/vpc.test.ts.snap b/source/packages/@aws-accelerator/constructs/test/aws-ec2/__snapshots__/vpc.test.ts.snap new file mode 100644 index 000000000..41811cd87 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-ec2/__snapshots__/vpc.test.ts.snap @@ -0,0 +1,58 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Vpc Construct(Vpc): Snapshot Test 1`] = ` +Object { + "Resources": Object { + "TestVpcDhcpOptionsAssociationDB23B751": Object { + "Properties": Object { + "DhcpOptionsId": "Test-Options", + "VpcId": Object { + "Ref": "TestVpcE77CE678", + }, + }, + "Type": "AWS::EC2::VPCDHCPOptionsAssociation", + }, + "TestVpcE77CE678": Object { + "Properties": Object { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": false, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": Array [ + Object { + "Key": "Name", + "Value": "Main", + }, + Object { + "Key": "Test-Key", + "Value": "Test-Value", + }, + ], + }, + "Type": "AWS::EC2::VPC", + }, + "TestVpcInternetGateway01360C82": Object { + "Properties": Object { + "Tags": Array [ + Object { + "Key": "Name", + "Value": "Main", + }, + ], + }, + "Type": "AWS::EC2::InternetGateway", + }, + "TestVpcInternetGatewayAttachment60E451D5": Object { + "Properties": Object { + "InternetGatewayId": Object { + "Ref": "TestVpcInternetGateway01360C82", + }, + "VpcId": Object { + "Ref": "TestVpcE77CE678", + }, + }, + "Type": "AWS::EC2::VPCGatewayAttachment", + }, + }, +} +`; diff --git a/source/packages/@aws-accelerator/constructs/test/aws-ec2/delete-default-vpc.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-ec2/delete-default-vpc.test.ts new file mode 100644 index 000000000..44d91d4e0 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-ec2/delete-default-vpc.test.ts @@ -0,0 +1,160 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { DeleteDefaultVpc } from '../../lib/aws-ec2/delete-default-vpc'; + +const testNamePrefix = 'Construct(DeleteDefaultVpc): '; + +//Initialize stack for snapshot test and resource configuration test +const stack = new cdk.Stack(); + +new DeleteDefaultVpc(stack, 'DeleteDefaultVpc', { + kmsKey: new cdk.aws_kms.Key(stack, 'CustomKey', {}), + logRetentionInDays: 3653, +}); + +/** + * DeleteDefaultVpc construct test + */ +describe('DeleteDefaultVpc', () => { + /** + * Number of IAM role resource test + */ + test(`${testNamePrefix} IAM role resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 1); + }); + + /** + * Number of Lambda function resource test + */ + test(`${testNamePrefix} Lambda function resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 1); + }); + + /** + * Number of DeleteDefaultVpc custom resource test + */ + test(`${testNamePrefix} DeleteDefaultVpc custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::DeleteDefaultVpc', 1); + }); + + /** + * Lambda Function resource configuration test + */ + test(`${testNamePrefix} Lambda Function resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomDeleteDefaultVpcCustomResourceProviderHandler87E89F35: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomDeleteDefaultVpcCustomResourceProviderRole80963EEF'], + Properties: { + Code: { + S3Bucket: { + 'Fn::Sub': 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', + }, + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomDeleteDefaultVpcCustomResourceProviderRole80963EEF', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * IAM role resource configuration test + */ + test(`${testNamePrefix} IAM role resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomDeleteDefaultVpcCustomResourceProviderRole80963EEF: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: [ + 'ec2:DeleteInternetGateway', + 'ec2:DetachInternetGateway', + 'ec2:DeleteNetworkAcl', + 'ec2:DeleteRoute', + 'ec2:DeleteSecurityGroup', + 'ec2:DeleteSubnet', + 'ec2:DeleteVpc', + 'ec2:DescribeInternetGateways', + 'ec2:DescribeNetworkAcls', + 'ec2:DescribeRouteTables', + 'ec2:DescribeSecurityGroups', + 'ec2:DescribeSubnets', + 'ec2:DescribeVpcs', + ], + Effect: 'Allow', + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * DeleteDefaultVpc custom resource configuration test + */ + test(`${testNamePrefix} DeleteDefaultVpc custom resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + DeleteDefaultVpc4DBAE36C: { + Type: 'Custom::DeleteDefaultVpc', + DeletionPolicy: 'Delete', + UpdateReplacePolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomDeleteDefaultVpcCustomResourceProviderHandler87E89F35', 'Arn'], + }, + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-ec2/dhcp-options.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-ec2/dhcp-options.test.ts new file mode 100644 index 000000000..14d534e43 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-ec2/dhcp-options.test.ts @@ -0,0 +1,78 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; + +import { SynthUtils } from '@aws-cdk/assert'; + +import { DhcpOptions } from '../../lib/aws-ec2/dhcp-options'; + +const testNamePrefix = 'Construct(DhcpOptions): '; + +//Initialize stack for snapshot test and resource configuration test +const stack = new cdk.Stack(); + +new DhcpOptions(stack, 'TestDhcpOpts', { + name: 'Test', + domainName: 'test.com', + domainNameServers: ['1.1.1.1'], + netbiosNameServers: ['1.1.1.1'], + netbiosNodeType: 2, + ntpServers: ['1.1.1.1'], + tags: [], +}); + +/** + * DHCP Options construct test + */ +describe('DhcpOptions', () => { + /** + * Snapshot test + */ + test(`${testNamePrefix} Snapshot Test`, () => { + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); + }); + + /** + * Number of DHCP options test + */ + test(`${testNamePrefix} DHCP options count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::EC2::DHCPOptions', 1); + }); + + /** + * DHCP options resource configuration test + */ + test(`${testNamePrefix} DHCP options resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TestDhcpOpts22CADF8A: { + Type: 'AWS::EC2::DHCPOptions', + Properties: { + DomainName: 'test.com', + DomainNameServers: ['1.1.1.1'], + NetbiosNameServers: ['1.1.1.1'], + NetbiosNodeType: 2, + NtpServers: ['1.1.1.1'], + Tags: [ + { + Key: 'Name', + Value: 'Test', + }, + ], + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-ec2/prefix-list.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-ec2/prefix-list.test.ts new file mode 100644 index 000000000..c402bb4ec --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-ec2/prefix-list.test.ts @@ -0,0 +1,68 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; + +import { SynthUtils } from '@aws-cdk/assert'; + +import { PrefixList } from '../../lib/aws-ec2/prefix-list'; + +const testNamePrefix = 'Construct(PrefixList): '; + +//Initialize stack for snapshot test and resource configuration test +const stack = new cdk.Stack(); + +new PrefixList(stack, 'TestPrefixList', { + name: 'Test', + addressFamily: 'IPv4', + maxEntries: 1, + entries: ['1.1.1.1/32'], + tags: [{ key: 'Test-Key', value: 'Test-Value' }], +}); + +/** + * Prefix List construct test + */ +describe('PrefixList', () => { + /** + * Snapshot test + */ + test(`${testNamePrefix} Snapshot Test`, () => { + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); + }); + + /** + * Number of DHCP options test + */ + test(`${testNamePrefix} Prefix List count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::EC2::PrefixList', 1); + }); + + /** + * DHCP options resource configuration test + */ + test(`${testNamePrefix} Prefix List resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TestPrefixListF3A076C9: { + Type: 'AWS::EC2::PrefixList', + Properties: { + AddressFamily: 'IPv4', + MaxEntries: 1, + Entries: [{ Cidr: '1.1.1.1/32' }], + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-ec2/route-table.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-ec2/route-table.test.ts new file mode 100644 index 000000000..79c63a62c --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-ec2/route-table.test.ts @@ -0,0 +1,164 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { SynthUtils } from '@aws-cdk/assert'; +import { RouteTable } from '../../lib/aws-ec2/route-table'; +import { Vpc } from '../../lib/aws-ec2/vpc'; + +const testNamePrefix = 'Construct(RouteTable): '; + +//Initialize stack for snapshot test and resource configuration test +const stack = new cdk.Stack(); + +const vpc = new Vpc(stack, 'TestVpc', { + name: 'Test', + ipv4CidrBlock: '10.0.0.0/16', + internetGateway: true, + enableDnsHostnames: false, + enableDnsSupport: true, + instanceTenancy: 'default', +}); + +new RouteTable(stack, 'RouteTable', { + name: 'TestRouteTable', + vpc: vpc, + tags: [{ key: 'Test-Key', value: 'Test-Value' }], +}); + +/** + * RouteTable construct test + */ +describe('RouteTable', () => { + /** + * Snapshot test + */ + test(`${testNamePrefix} Snapshot Test`, () => { + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); + }); + + /** + * Number of RouteTable test + */ + test(`${testNamePrefix} RouteTable count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::EC2::RouteTable', 1); + }); + + /** + * Number of VPC test + */ + test(`${testNamePrefix} VPC count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::EC2::VPC', 1); + }); + + /** + * Number of InternetGateway test + */ + test(`${testNamePrefix} InternetGateway count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::EC2::InternetGateway', 1); + }); + + /** + * Number of VPCGatewayAttachment test + */ + test(`${testNamePrefix} VPCGatewayAttachment count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::EC2::VPCGatewayAttachment', 1); + }); + + /** + * RouteTable resource configuration test + */ + test(`${testNamePrefix} RouteTable resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + RouteTable82FB8FA6: { + Type: 'AWS::EC2::RouteTable', + Properties: { + Tags: [ + { + Key: 'Name', + Value: 'TestRouteTable', + }, + { + Key: 'Test-Key', + Value: 'Test-Value', + }, + ], + VpcId: { + Ref: 'TestVpcE77CE678', + }, + }, + }, + }, + }); + }); + + /** + * VPC resource configuration test + */ + test(`${testNamePrefix} VPC resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TestVpcE77CE678: { + Type: 'AWS::EC2::VPC', + Properties: { + CidrBlock: '10.0.0.0/16', + EnableDnsHostnames: false, + EnableDnsSupport: true, + InstanceTenancy: 'default', + Tags: [ + { + Key: 'Name', + Value: 'Test', + }, + ], + }, + }, + }, + }); + }); + + /** + * InternetGateway resource configuration test + */ + test(`${testNamePrefix} InternetGateway resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TestVpcInternetGateway01360C82: { + Type: 'AWS::EC2::InternetGateway', + }, + }, + }); + }); + + /** + * VPCGatewayAttachment resource configuration test + */ + test(`${testNamePrefix} VPCGatewayAttachment resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TestVpcInternetGatewayAttachment60E451D5: { + Type: 'AWS::EC2::VPCGatewayAttachment', + Properties: { + InternetGatewayId: { + Ref: 'TestVpcInternetGateway01360C82', + }, + VpcId: { + Ref: 'TestVpcE77CE678', + }, + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-ec2/transit-gateway-route-table.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-ec2/transit-gateway-route-table.test.ts new file mode 100644 index 000000000..0550d282b --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-ec2/transit-gateway-route-table.test.ts @@ -0,0 +1,70 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { SynthUtils } from '@aws-cdk/assert'; +import { TransitGatewayRouteTable } from '../../lib/aws-ec2/transit-gateway-route-table'; + +const testNamePrefix = 'Construct(TransitGatewayRouteTable): '; + +//Initialize stack for snapshot test and resource configuration test +const stack = new cdk.Stack(); + +new TransitGatewayRouteTable(stack, 'TransitGatewayRouteTable', { + name: 'core', + transitGatewayId: 'tgw0001', + tags: [{ key: 'Test-Key', value: 'Test-Value' }], +}); +/** + * TransitGatewayRouteTable construct test + */ +describe('TransitGatewayRouteTable', () => { + /** + * Snapshot test + */ + test(`${testNamePrefix} Snapshot Test`, () => { + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); + }); + /** + * Number of TransitGatewayRouteTable resource test + */ + test(`${testNamePrefix} TransitGatewayRouteTable resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::EC2::TransitGatewayRouteTable', 1); + }); + + /** + * TransitGatewayRouteTable resource configuration test + */ + test(`${testNamePrefix} TransitGatewayRouteTable resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TransitGatewayRouteTableCoreTransitGatewayRouteTableD6BC94E0: { + Type: 'AWS::EC2::TransitGatewayRouteTable', + Properties: { + Tags: [ + { + Key: 'Name', + Value: 'core', + }, + { + Key: 'Test-Key', + Value: 'Test-Value', + }, + ], + TransitGatewayId: 'tgw0001', + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-ec2/transit-gateway.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-ec2/transit-gateway.test.ts new file mode 100644 index 000000000..11b7201a4 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-ec2/transit-gateway.test.ts @@ -0,0 +1,60 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { SynthUtils } from '@aws-cdk/assert'; +import { TransitGatewayRouteTableAssociation } from '../../lib/aws-ec2/transit-gateway'; + +const testNamePrefix = 'Construct(TransitGatewayRouteTableAssociation): '; + +//Initialize stack for snapshot test and resource configuration test +const stack = new cdk.Stack(); + +new TransitGatewayRouteTableAssociation(stack, 'TransitGatewayRouteTableAssociation', { + transitGatewayAttachmentId: 'transitGatewayAttachmentId', + transitGatewayRouteTableId: 'transitGatewayRouteTableId', +}); +/** + * TransitGatewayRouteTableAssociation construct test + */ +describe('TransitGatewayRouteTableAssociation', () => { + /** + * Snapshot test + */ + test(`${testNamePrefix} Snapshot Test`, () => { + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); + }); + /** + * Number of TransitGatewayRouteTableAssociation resource test + */ + test(`${testNamePrefix} TransitGatewayRouteTableAssociation resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::EC2::TransitGatewayRouteTableAssociation', 1); + }); + + /** + * TransitGatewayRouteTableAssociation resource configuration test + */ + test(`${testNamePrefix} TransitGatewayRouteTableAssociation resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TransitGatewayRouteTableAssociation19E386E4: { + Type: 'AWS::EC2::TransitGatewayRouteTableAssociation', + Properties: { + TransitGatewayAttachmentId: 'transitGatewayAttachmentId', + TransitGatewayRouteTableId: 'transitGatewayRouteTableId', + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-ec2/vpc-endpoint.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-ec2/vpc-endpoint.test.ts new file mode 100644 index 000000000..5cbe22c54 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-ec2/vpc-endpoint.test.ts @@ -0,0 +1,154 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; + +import { SynthUtils } from '@aws-cdk/assert'; + +import { SecurityGroup } from '../../lib/aws-ec2/vpc'; +import { VpcEndpoint } from '../../lib/aws-ec2/vpc-endpoint'; + +const testNamePrefix = 'Construct(VpcEndpoint): '; + +//Initialize stack for snapshot test and resource configuration test +const stack = new cdk.Stack(); + +const securityGroup = new SecurityGroup(stack, 'TestSecurityGroup`', { + securityGroupName: 'TestSecurityGroup', + description: `AWS Private Endpoint Zone`, + vpcId: 'Test', +}); + +new VpcEndpoint(stack, 'VpcEndpoint', { + vpcId: 'Test', + vpcEndpointType: cdk.aws_ec2.VpcEndpointType.GATEWAY, + service: 'service', + subnets: ['Test1', 'Test2'], + securityGroups: [securityGroup], + privateDnsEnabled: true, + policyDocument: new cdk.aws_iam.PolicyDocument({ + statements: [ + new cdk.aws_iam.PolicyStatement({ + sid: 'AccessToTrustedPrincipalsAndResources', + actions: ['*'], + effect: cdk.aws_iam.Effect.ALLOW, + resources: ['*'], + principals: [new cdk.aws_iam.AnyPrincipal()], + conditions: { + StringEquals: { + 'aws:PrincipalOrgID': ['organizationId'], + }, + }, + }), + ], + }), + routeTables: ['Test1', 'Test2'], +}); + +/** + * VpcEndpoint construct test + */ +describe('VpcEndpoint', () => { + /** + * Snapshot test + */ + test(`${testNamePrefix} Snapshot Test`, () => { + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); + }); + + /** + * Number of SecurityGroup test + */ + test(`${testNamePrefix} SecurityGroup resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::EC2::SecurityGroup', 1); + }); + + /** + * Number of VPCEndpoint test + */ + test(`${testNamePrefix} VPCEndpoint resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::EC2::VPCEndpoint', 1); + }); + + /** + * SecurityGroup resource configuration test + */ + test(`${testNamePrefix} SecurityGroup resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TestSecurityGroupDA4B5F83: { + Type: 'AWS::EC2::SecurityGroup', + Properties: { + GroupDescription: 'AWS Private Endpoint Zone', + GroupName: 'TestSecurityGroup', + Tags: [ + { + Key: 'Name', + Value: 'TestSecurityGroup', + }, + ], + VpcId: 'Test', + }, + }, + }, + }); + }); + + /** + * VPC endpoint resource configuration test + */ + test(`${testNamePrefix} VPC endpoint resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + VpcEndpoint80208C18: { + Type: 'AWS::EC2::VPCEndpoint', + Properties: { + PolicyDocument: { + Statement: [ + { + Action: '*', + Condition: { + StringEquals: { + 'aws:PrincipalOrgID': ['organizationId'], + }, + }, + Effect: 'Allow', + Principal: { + AWS: '*', + }, + Resource: '*', + Sid: 'AccessToTrustedPrincipalsAndResources', + }, + ], + Version: '2012-10-17', + }, + RouteTableIds: ['Test1', 'Test2'], + ServiceName: { + 'Fn::Join': [ + '', + [ + 'com.amazonaws.', + { + Ref: 'AWS::Region', + }, + '.service', + ], + ], + }, + VpcId: 'Test', + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-ec2/vpc-peering.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-ec2/vpc-peering.test.ts new file mode 100644 index 000000000..4fc32487a --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-ec2/vpc-peering.test.ts @@ -0,0 +1,71 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; + +import { VpcPeering } from '../../lib/aws-ec2/vpc-peering'; + +const testNamePrefix = 'Construct(VpcPeering): '; + +//Initialize stack for tests +const stack = new cdk.Stack(); + +new VpcPeering(stack, 'TestPeering', { + name: 'Test', + peerOwnerId: '111111111111', + peerRegion: 'us-east-1', + peerVpcId: 'AccepterVpc', + vpcId: 'RequesterVpc', + peerRoleName: 'TestRole', + tags: [], +}); + +/** + * VPC peering construct test + */ +describe('VpcPeering', () => { + /** + * Number of VPC peering test + */ + test(`${testNamePrefix} VPC peering count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::EC2::VPCPeeringConnection', 1); + }); + + /** + * VPC peering resource configuration test + */ + test(`${testNamePrefix} VPC peering resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TestPeeringF63C5812: { + Type: 'AWS::EC2::VPCPeeringConnection', + Properties: { + PeerOwnerId: '111111111111', + PeerRegion: 'us-east-1', + PeerRoleArn: { + 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::111111111111:role/TestRole']], + }, + PeerVpcId: 'AccepterVpc', + Tags: [ + { + Key: 'Name', + Value: 'Test', + }, + ], + VpcId: 'RequesterVpc', + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-ec2/vpc.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-ec2/vpc.test.ts new file mode 100644 index 000000000..eb74e3177 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-ec2/vpc.test.ts @@ -0,0 +1,156 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; + +import { SynthUtils } from '@aws-cdk/assert'; + +import { Vpc } from '../../lib/aws-ec2/vpc'; + +const testNamePrefix = 'Construct(Vpc): '; + +//Initialize stack for snapshot test and resource configuration test +const stack = new cdk.Stack(); + +new Vpc(stack, 'TestVpc', { + name: 'Main', + ipv4CidrBlock: '10.0.0.0/16', + dhcpOptions: 'Test-Options', + internetGateway: true, + enableDnsHostnames: false, + enableDnsSupport: true, + instanceTenancy: 'default', + tags: [{ key: 'Test-Key', value: 'Test-Value' }], +}); + +/** + * Vpc construct test + */ +describe('Vpc', () => { + /** + * Snapshot test + */ + test(`${testNamePrefix} Snapshot Test`, () => { + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); + }); + + /** + * Number of VPC test + */ + test(`${testNamePrefix} VPC count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::EC2::VPC', 1); + }); + + /** + * Number of InternetGateway test + */ + test(`${testNamePrefix} InternetGateway count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::EC2::InternetGateway', 1); + }); + + /** + * Number of DHCP options test + */ + test(`${testNamePrefix} DHCP options association count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::EC2::VPCDHCPOptionsAssociation', 1); + }); + + /** + * Number of VPCGatewayAttachment test + */ + test(`${testNamePrefix} VPCGatewayAttachment count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::EC2::VPCGatewayAttachment', 1); + }); + + /** + * VPC resource configuration test + */ + test(`${testNamePrefix} VPC resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TestVpcE77CE678: { + Type: 'AWS::EC2::VPC', + Properties: { + CidrBlock: '10.0.0.0/16', + EnableDnsHostnames: false, + EnableDnsSupport: true, + InstanceTenancy: 'default', + Tags: [ + { + Key: 'Name', + Value: 'Main', + }, + { + Key: 'Test-Key', + Value: 'Test-Value', + }, + ], + }, + }, + }, + }); + }); + + /** + * InternetGateway resource configuration test + */ + test(`${testNamePrefix} InternetGateway resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TestVpcInternetGateway01360C82: { + Type: 'AWS::EC2::InternetGateway', + }, + }, + }); + }); + + /** + * DHCP options association resource configuration test + */ + test(`${testNamePrefix} DHCP options association resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TestVpcDhcpOptionsAssociationDB23B751: { + Type: 'AWS::EC2::VPCDHCPOptionsAssociation', + Properties: { + DhcpOptionsId: 'Test-Options', + VpcId: { + Ref: 'TestVpcE77CE678', + }, + }, + }, + }, + }); + }); + + /** + * VPCGatewayAttachment resource configuration test + */ + test(`${testNamePrefix} VPCGatewayAttachment resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TestVpcInternetGatewayAttachment60E451D5: { + Type: 'AWS::EC2::VPCGatewayAttachment', + Properties: { + InternetGatewayId: { + Ref: 'TestVpcInternetGateway01360C82', + }, + VpcId: { + Ref: 'TestVpcE77CE678', + }, + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-guardduty/guardduty-detector-config.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-guardduty/guardduty-detector-config.test.ts new file mode 100644 index 000000000..222e91e99 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-guardduty/guardduty-detector-config.test.ts @@ -0,0 +1,161 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { GuardDutyDetectorConfig } from '../../lib/aws-guardduty/guardduty-detector-config'; + +const testNamePrefix = 'Construct(GuardDutyDetectorConfig): '; + +//Initialize stack for snapshot test and resource configuration test +const stack = new cdk.Stack(); + +new GuardDutyDetectorConfig(stack, 'GuardDutyDetectorConfig', { + isExportConfigEnable: true, + exportDestination: 'S3', + exportFrequency: 'FIFTEEN_MINUTES', + kmsKey: new cdk.aws_kms.Key(stack, 'CustomKey', {}), + logRetentionInDays: 3653, +}); + +/** + * GuardDutyDetectorConfig construct test + */ +describe('GuardDutyDetectorConfig', () => { + /** + * Number of IAM role resource test + */ + test(`${testNamePrefix} IAM role resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 1); + }); + + /** + * Number of Lambda function resource test + */ + test(`${testNamePrefix} Lambda function resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 1); + }); + + /** + * Number of GuardDutyUpdateDetector custom resource test + */ + test(`${testNamePrefix} GuardDutyUpdateDetector custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::GuardDutyUpdateDetector', 1); + }); + + /** + * Lambda Function resource configuration test + */ + test(`${testNamePrefix} Lambda Function resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomGuardDutyUpdateDetectorCustomResourceProviderHandler78DF0FF9: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomGuardDutyUpdateDetectorCustomResourceProviderRole3014073E'], + Properties: { + Code: { + S3Bucket: { + 'Fn::Sub': 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', + }, + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomGuardDutyUpdateDetectorCustomResourceProviderRole3014073E', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * IAM role resource configuration test + */ + test(`${testNamePrefix} IAM role resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomGuardDutyUpdateDetectorCustomResourceProviderRole3014073E: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: [ + 'guardduty:ListDetectors', + 'guardduty:ListMembers', + 'guardduty:UpdateDetector', + 'guardduty:UpdateMemberDetectors', + ], + Effect: 'Allow', + Resource: '*', + Sid: 'GuardDutyUpdateDetectorTaskGuardDutyActions', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * GuardDutyUpdateDetector custom resource configuration test + */ + test(`${testNamePrefix} GuardDutyUpdateDetector custom resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + GuardDutyDetectorConfigDD64B103: { + Type: 'Custom::GuardDutyUpdateDetector', + DeletionPolicy: 'Delete', + UpdateReplacePolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomGuardDutyUpdateDetectorCustomResourceProviderHandler78DF0FF9', 'Arn'], + }, + exportDestination: 'S3', + exportFrequency: 'FIFTEEN_MINUTES', + isExportConfigEnable: true, + region: { + Ref: 'AWS::Region', + }, + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-guardduty/guardduty-members.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-guardduty/guardduty-members.test.ts new file mode 100644 index 000000000..de0f24cba --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-guardduty/guardduty-members.test.ts @@ -0,0 +1,178 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { GuardDutyMembers } from '../../lib/aws-guardduty/guardduty-members'; + +const testNamePrefix = 'Construct(GuardDutyMembers): '; + +//Initialize stack for snapshot test and resource configuration test +const stack = new cdk.Stack(); + +new GuardDutyMembers(stack, 'GuardDutyMembers', { + enableS3Protection: true, + kmsKey: new cdk.aws_kms.Key(stack, 'CustomKey', {}), + logRetentionInDays: 3653, +}); + +/** + * GuardDutyMembers construct test + */ +describe('GuardDutyMembers', () => { + /** + * Number of IAM role resource test + */ + test(`${testNamePrefix} IAM role resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 1); + }); + + /** + * Number of Lambda function resource test + */ + test(`${testNamePrefix} Lambda function resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 1); + }); + + /** + * Number of GuardDutyCreateMembers custom resource test + */ + test(`${testNamePrefix} GuardDutyCreateMembers custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::GuardDutyCreateMembers', 1); + }); + + /** + * Lambda Function resource configuration test + */ + test(`${testNamePrefix} Lambda Function resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomGuardDutyCreateMembersCustomResourceProviderHandler0A16C673: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomGuardDutyCreateMembersCustomResourceProviderRole2D82020E'], + Properties: { + Code: { + S3Bucket: { + 'Fn::Sub': 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', + }, + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomGuardDutyCreateMembersCustomResourceProviderRole2D82020E', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * IAM role resource configuration test + */ + // test(`${testNamePrefix} IAM role resource configuration test`, () => { + // cdk.assertions.Template.fromStack(stack).templateMatches({ + // Resources: { + // CustomGuardDutyCreateMembersCustomResourceProviderRole2D82020E: { + // Type: 'AWS::IAM::Role', + // Properties: { + // AssumeRolePolicyDocument: { + // Statement: [ + // { + // Action: 'sts:AssumeRole', + // Effect: 'Allow', + // Principal: { + // Service: 'lambda.amazonaws.com', + // }, + // }, + // ], + // Version: '2012-10-17', + // }, + // ManagedPolicyArns: [ + // { + // 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + // }, + // ], + // Policies: [ + // { + // PolicyDocument: { + // Statement: [ + // { + // Action: ['organizations:ListAccounts'], + // Condition: { + // StringLikeIfExists: { + // 'organizations:ListAccounts': ['guardduty.amazonaws.com'], + // }, + // }, + // Effect: 'Allow', + // Resource: '*', + // Sid: 'GuardDutyCreateMembersTaskOrganizationAction', + // }, + // { + // Action: [ + // 'guardDuty:ListDetectors', + // 'guardDuty:ListOrganizationAdminAccounts', + // 'guardDuty:UpdateOrganizationConfiguration', + // 'guardduty:CreateMembers', + // 'guardduty:DeleteMembers', + // 'guardduty:DisassociateMembers', + // 'guardduty:ListDetectors', + // 'guardduty:ListMembers', + // ], + // Effect: 'Allow', + // Resource: '*', + // Sid: 'GuardDutyCreateMembersTaskGuardDutyActions', + // }, + // { + // Action: ['iam:CreateServiceLinkedRole'], + // Effect: 'Allow', + // Resource: ['*'], + // Sid: 'ServiceLinkedRoleSecurityHub', + // }, + // ], + // Version: '2012-10-17', + // }, + // PolicyName: 'Inline', + // }, + // ], + // }, + // }, + // }, + // }); + // }); + + /** + * GuardDutyCreateMembers custom resource configuration test + */ + test(`${testNamePrefix} GuardDutyCreateMembers custom resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + GuardDutyMembersD34CA003: { + Type: 'Custom::GuardDutyCreateMembers', + DeletionPolicy: 'Delete', + UpdateReplacePolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomGuardDutyCreateMembersCustomResourceProviderHandler0A16C673', 'Arn'], + }, + enableS3Protection: true, + region: { + Ref: 'AWS::Region', + }, + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-guardduty/guardduty-organization-admin-account.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-guardduty/guardduty-organization-admin-account.test.ts new file mode 100644 index 000000000..bac29f458 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-guardduty/guardduty-organization-admin-account.test.ts @@ -0,0 +1,190 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { GuardDutyOrganizationAdminAccount } from '../../lib/aws-guardduty/guardduty-organization-admin-account'; + +const testNamePrefix = 'Construct(GuardDutyOrganizationAdminAccount): '; + +//Initialize stack for snapshot test and resource configuration test +const stack = new cdk.Stack(); + +new GuardDutyOrganizationAdminAccount(stack, 'GuardDutyOrganizationAdminAccount', { + adminAccountId: stack.account, + kmsKey: new cdk.aws_kms.Key(stack, 'CustomKey', {}), + logRetentionInDays: 3653, +}); + +/** + * GuardDutyOrganizationAdminAccount construct test + */ +describe('GuardDutyOrganizationAdminAccount', () => { + /** + * Number of IAM role resource test + */ + test(`${testNamePrefix} IAM role resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 1); + }); + + /** + * Number of Lambda function resource test + */ + test(`${testNamePrefix} Lambda function resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 1); + }); + + /** + * Number of GuardDutyEnableOrganizationAdminAccount custom resource test + */ + test(`${testNamePrefix} GuardDutyEnableOrganizationAdminAccount custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::GuardDutyEnableOrganizationAdminAccount', 1); + }); + + /** + * Lambda Function resource configuration test + */ + test(`${testNamePrefix} Lambda Function resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomGuardDutyEnableOrganizationAdminAccountCustomResourceProviderHandler1EC01026: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomGuardDutyEnableOrganizationAdminAccountCustomResourceProviderRole30371E09'], + Properties: { + Code: { + S3Bucket: { + 'Fn::Sub': 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', + }, + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomGuardDutyEnableOrganizationAdminAccountCustomResourceProviderRole30371E09', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * IAM role resource configuration test + */ + test(`${testNamePrefix} IAM role resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomGuardDutyEnableOrganizationAdminAccountCustomResourceProviderRole30371E09: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: [ + 'organizations:DeregisterDelegatedAdministrator', + 'organizations:DescribeOrganization', + 'organizations:EnableAWSServiceAccess', + 'organizations:ListAWSServiceAccessForOrganization', + 'organizations:ListAccounts', + 'organizations:ListDelegatedAdministrators', + 'organizations:RegisterDelegatedAdministrator', + 'organizations:ServicePrincipal', + 'organizations:UpdateOrganizationConfiguration', + ], + Condition: { + StringLikeIfExists: { + 'organizations:DeregisterDelegatedAdministrator': ['guardduty.amazonaws.com'], + 'organizations:DescribeOrganization': ['guardduty.amazonaws.com'], + 'organizations:EnableAWSServiceAccess': ['guardduty.amazonaws.com'], + 'organizations:ListAWSServiceAccessForOrganization': ['guardduty.amazonaws.com'], + 'organizations:ListAccounts': ['guardduty.amazonaws.com'], + 'organizations:ListDelegatedAdministrators': ['guardduty.amazonaws.com'], + 'organizations:RegisterDelegatedAdministrator': ['guardduty.amazonaws.com'], + 'organizations:ServicePrincipal': ['guardduty.amazonaws.com'], + 'organizations:UpdateOrganizationConfiguration': ['guardduty.amazonaws.com'], + }, + }, + Effect: 'Allow', + Resource: '*', + Sid: 'GuardDutyEnableOrganizationAdminAccountTaskOrganizationActions', + }, + { + Action: [ + 'GuardDuty:EnableOrganizationAdminAccount', + 'GuardDuty:ListOrganizationAdminAccounts', + 'guardduty:DisableOrganizationAdminAccount', + ], + Effect: 'Allow', + Resource: '*', + Sid: 'GuardDutyEnableOrganizationAdminAccountTaskGuardDutyActions', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * GuardDutyEnableOrganizationAdminAccount custom resource configuration test + */ + test(`${testNamePrefix} GuardDutyEnableOrganizationAdminAccount custom resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + GuardDutyOrganizationAdminAccount457DB4F1: { + Type: 'Custom::GuardDutyEnableOrganizationAdminAccount', + DeletionPolicy: 'Delete', + UpdateReplacePolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': [ + 'CustomGuardDutyEnableOrganizationAdminAccountCustomResourceProviderHandler1EC01026', + 'Arn', + ], + }, + adminAccountId: { + Ref: 'AWS::AccountId', + }, + region: { + Ref: 'AWS::Region', + }, + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-guardduty/guardduty-publishing-destination.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-guardduty/guardduty-publishing-destination.test.ts new file mode 100644 index 000000000..e115231af --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-guardduty/guardduty-publishing-destination.test.ts @@ -0,0 +1,188 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { GuardDutyPublishingDestination } from '../../lib/aws-guardduty/guardduty-publishing-destination'; + +const testNamePrefix = 'Construct(GuardDutyPublishingDestination): '; + +//Initialize stack for snapshot test and resource configuration test +const stack = new cdk.Stack(); + +new GuardDutyPublishingDestination(stack, 'GuardDutyPublishingDestination', { + bucketArn: `arn:${stack.partition}:s3:::aws-accelerator-org-gduty-pub-dest-${stack.account}-${stack.region}`, + exportDestinationType: 'S3', + kmsKey: new cdk.aws_kms.Key(stack, 'CustomKey', {}), + logRetentionInDays: 3653, +}); + +/** + * GuardDutyPublishingDestination construct test + */ +describe('GuardDutyPublishingDestination', () => { + /** + * Number of IAM role resource test + */ + test(`${testNamePrefix} IAM role resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 1); + }); + + /** + * Number of Lambda function resource test + */ + test(`${testNamePrefix} Lambda function resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 1); + }); + + /** + * Number of GuardDutyCreatePublishingDestinationCommand custom resource test + */ + test(`${testNamePrefix} GuardDutyCreatePublishingDestinationCommand custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::GuardDutyCreatePublishingDestinationCommand', 1); + }); + + /** + * Lambda Function resource configuration test + */ + test(`${testNamePrefix} Lambda Function resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomGuardDutyCreatePublishingDestinationCommandCustomResourceProviderHandlerB3AE4CE8: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomGuardDutyCreatePublishingDestinationCommandCustomResourceProviderRoleD01DD26B'], + Properties: { + Code: { + S3Bucket: { + 'Fn::Sub': 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', + }, + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': [ + 'CustomGuardDutyCreatePublishingDestinationCommandCustomResourceProviderRoleD01DD26B', + 'Arn', + ], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * IAM role resource configuration test + */ + test(`${testNamePrefix} IAM role resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomGuardDutyCreatePublishingDestinationCommandCustomResourceProviderRoleD01DD26B: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: [ + 'guardDuty:CreateDetector', + 'guardDuty:CreatePublishingDestination', + 'guardDuty:DeletePublishingDestination', + 'guardDuty:ListDetectors', + 'guardDuty:ListPublishingDestinations', + 'iam:CreateServiceLinkedRole', + ], + Effect: 'Allow', + Resource: '*', + Sid: 'GuardDutyCreatePublishingDestinationCommandTaskGuardDutyActions', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * GuardDutyCreatePublishingDestinationCommand custom resource configuration test + */ + test(`${testNamePrefix} GuardDutyCreatePublishingDestinationCommand custom resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + GuardDutyPublishingDestination52AE4412: { + Type: 'Custom::GuardDutyCreatePublishingDestinationCommand', + UpdateReplacePolicy: 'Delete', + DeletionPolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': [ + 'CustomGuardDutyCreatePublishingDestinationCommandCustomResourceProviderHandlerB3AE4CE8', + 'Arn', + ], + }, + bucketArn: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':s3:::aws-accelerator-org-gduty-pub-dest-', + { + Ref: 'AWS::AccountId', + }, + '-', + { + Ref: 'AWS::Region', + }, + ], + ], + }, + exportDestinationType: 'S3', + kmsKeyArn: { + 'Fn::GetAtt': ['CustomKey1E6D0D07', 'Arn'], + }, + region: { + Ref: 'AWS::Region', + }, + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-iam/password-policy.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-iam/password-policy.test.ts new file mode 100644 index 000000000..ea8cf0398 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-iam/password-policy.test.ts @@ -0,0 +1,164 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { PasswordPolicy } from '@aws-accelerator/constructs'; + +const testNamePrefix = 'Construct(CentralLogsBucket): '; + +//Initialize stack for snapshot test and resource configuration test +const stack = new cdk.Stack(); + +new PasswordPolicy(stack, 'PasswordPolicy', { + allowUsersToChangePassword: true, + hardExpiry: true, + requireUppercaseCharacters: true, + requireLowercaseCharacters: true, + requireSymbols: true, + requireNumbers: true, + minimumPasswordLength: 8, + passwordReusePrevention: 5, + maxPasswordAge: 90, + kmsKey: new cdk.aws_kms.Key(stack, 'CustomKey', {}), + logRetentionInDays: 3653, +}); + +/** + * PasswordPolicy construct test + */ +describe('PasswordPolicy', () => { + /** + * Number of IAM role resource test + */ + test(`${testNamePrefix} IAM role resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 1); + }); + + /** + * Number of Lambda function resource test + */ + test(`${testNamePrefix} Lambda function resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 1); + }); + + /** + * Number of IamUpdateAccountPasswordPolicy custom resource test + */ + test(`${testNamePrefix} IamUpdateAccountPasswordPolicy custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::IamUpdateAccountPasswordPolicy', 1); + }); + + /** + * Lambda Function resource configuration test + */ + test(`${testNamePrefix} Lambda Function resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomIamUpdateAccountPasswordPolicyCustomResourceProviderHandler63EDC7F4: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomIamUpdateAccountPasswordPolicyCustomResourceProviderRoleC4ECAFE0'], + Properties: { + Code: { + S3Bucket: { + 'Fn::Sub': 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', + }, + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomIamUpdateAccountPasswordPolicyCustomResourceProviderRoleC4ECAFE0', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * IAM role resource configuration test + */ + test(`${testNamePrefix} IAM role resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomIamUpdateAccountPasswordPolicyCustomResourceProviderRoleC4ECAFE0: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: ['iam:UpdateAccountPasswordPolicy'], + Effect: 'Allow', + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * IamUpdateAccountPasswordPolicy custom resource configuration test + */ + test(`${testNamePrefix} IamUpdateAccountPasswordPolicy custom resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + PasswordPolicy4B0A08FE: { + Type: 'Custom::IamUpdateAccountPasswordPolicy', + DeletionPolicy: 'Delete', + UpdateReplacePolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomIamUpdateAccountPasswordPolicyCustomResourceProviderHandler63EDC7F4', 'Arn'], + }, + allowUsersToChangePassword: true, + hardExpiry: true, + maxPasswordAge: 90, + minimumPasswordLength: 8, + passwordReusePrevention: 5, + requireLowercaseCharacters: true, + requireNumbers: true, + requireSymbols: true, + requireUppercaseCharacters: true, + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-macie/macie-members.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-macie/macie-members.test.ts new file mode 100644 index 000000000..8c16e0b73 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-macie/macie-members.test.ts @@ -0,0 +1,174 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; + +import { MacieMembers } from '../../index'; + +const testNamePrefix = 'Construct(MacieMembers): '; + +//Initialize stack for snapshot test and resource configuration test +const stack = new cdk.Stack(); + +new MacieMembers(stack, 'MacieMembers', { + adminAccountId: stack.account, + logRetentionInDays: 3653, + kmsKey: new cdk.aws_kms.Key(stack, 'Key', {}), +}); +/** + * MacieMembers construct test + */ +describe('MacieMembers', () => { + /** + * Number of IAM role resource test + */ + test(`${testNamePrefix} IAM role resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 1); + }); + + /** + * Number of Lambda function resource test + */ + test(`${testNamePrefix} Lambda function resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 1); + }); + + /** + * Number of MacieCreateMember custom resource test + */ + test(`${testNamePrefix} MacieCreateMember custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::MacieCreateMember', 1); + }); + + /** + * Lambda Function resource configuration test + */ + test(`${testNamePrefix} Lambda Function resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomMacieCreateMemberCustomResourceProviderHandler913F75DB: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomMacieCreateMemberCustomResourceProviderRole3E8977EE'], + Properties: { + Code: { + S3Bucket: { + 'Fn::Sub': 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', + }, + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomMacieCreateMemberCustomResourceProviderRole3E8977EE', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * IAM role resource configuration test + */ + test(`${testNamePrefix} IAM role resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomMacieCreateMemberCustomResourceProviderRole3E8977EE: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: ['organizations:ListAccounts'], + Condition: { + StringLikeIfExists: { + 'organizations:ListAccounts': ['macie.amazonaws.com'], + }, + }, + Effect: 'Allow', + Resource: '*', + Sid: 'MacieCreateMemberTaskOrganizationAction', + }, + { + Action: [ + 'macie2:CreateMember', + 'macie2:DeleteMember', + 'macie2:DescribeOrganizationConfiguration', + 'macie2:DisassociateMember', + 'macie2:EnableMacie', + 'macie2:GetMacieSession', + 'macie2:ListMembers', + 'macie2:UpdateOrganizationConfiguration', + ], + Effect: 'Allow', + Resource: '*', + Sid: 'MacieCreateMemberTaskMacieActions', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * MacieCreateMember custom resource configuration test + */ + test(`${testNamePrefix} MacieCreateMember custom resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + MacieMembers1B6840B4: { + Type: 'Custom::MacieCreateMember', + DeletionPolicy: 'Delete', + UpdateReplacePolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomMacieCreateMemberCustomResourceProviderHandler913F75DB', 'Arn'], + }, + adminAccountId: { + Ref: 'AWS::AccountId', + }, + region: { + Ref: 'AWS::Region', + }, + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-macie/macie-organization-admin-account.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-macie/macie-organization-admin-account.test.ts new file mode 100644 index 000000000..438788126 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-macie/macie-organization-admin-account.test.ts @@ -0,0 +1,202 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { MacieOrganizationAdminAccount } from '../../index'; + +const testNamePrefix = 'Construct(MacieOrganizationAdminAccount): '; + +//Initialize stack for snapshot test and resource configuration test +const stack = new cdk.Stack(); + +new MacieOrganizationAdminAccount(stack, 'MacieOrganizationAdminAccount', { + adminAccountId: stack.account, + logRetentionInDays: 3653, + kmsKey: new cdk.aws_kms.Key(stack, 'Key', {}), +}); +/** + * MacieOrganizationAdminAccount construct test + */ +describe('MacieOrganizationAdminAccount', () => { + /** + * Number of IAM role resource test + */ + test(`${testNamePrefix} IAM role resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 1); + }); + + /** + * Number of Lambda function resource test + */ + test(`${testNamePrefix} Lambda function resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 1); + }); + + /** + * Number of MacieEnableOrganizationAdminAccount custom resource test + */ + test(`${testNamePrefix} MacieEnableOrganizationAdminAccount custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::MacieEnableOrganizationAdminAccount', 1); + }); + + /** + * Lambda Function resource configuration test + */ + test(`${testNamePrefix} Lambda Function resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomMacieEnableOrganizationAdminAccountCustomResourceProviderHandlerD7A9976A: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomMacieEnableOrganizationAdminAccountCustomResourceProviderRoleA386B194'], + Properties: { + Code: { + S3Bucket: { + 'Fn::Sub': 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', + }, + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomMacieEnableOrganizationAdminAccountCustomResourceProviderRoleA386B194', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * IAM role resource configuration test + */ + test(`${testNamePrefix} IAM role resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomMacieEnableOrganizationAdminAccountCustomResourceProviderRoleA386B194: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: [ + 'organizations:DeregisterDelegatedAdministrator', + 'organizations:DescribeOrganization', + 'organizations:EnableAWSServiceAccess', + 'organizations:ListAWSServiceAccessForOrganization', + 'organizations:ListAccounts', + 'organizations:ListDelegatedAdministrators', + 'organizations:RegisterDelegatedAdministrator', + 'organizations:ServicePrincipal', + 'organizations:UpdateOrganizationConfiguration', + ], + Condition: { + StringLikeIfExists: { + 'organizations:DeregisterDelegatedAdministrator': ['macie.amazonaws.com'], + 'organizations:DescribeOrganization': ['macie.amazonaws.com'], + 'organizations:EnableAWSServiceAccess': ['macie.amazonaws.com'], + 'organizations:ListAWSServiceAccessForOrganization': ['macie.amazonaws.com'], + 'organizations:ListAccounts': ['macie.amazonaws.com'], + 'organizations:ListDelegatedAdministrators': ['macie.amazonaws.com'], + 'organizations:RegisterDelegatedAdministrator': ['macie.amazonaws.com'], + 'organizations:ServicePrincipal': ['macie.amazonaws.com'], + 'organizations:UpdateOrganizationConfiguration': ['macie.amazonaws.com'], + }, + }, + Effect: 'Allow', + Resource: '*', + Sid: 'MacieEnableOrganizationAdminAccountTaskOrganizationActions', + }, + { + Action: [ + 'macie2:DisableOrganizationAdminAccount', + 'macie2:EnableMacie', + 'macie2:EnableOrganizationAdminAccount', + 'macie2:GetMacieSession', + 'macie2:ListOrganizationAdminAccounts', + 'macie2:DisableOrganizationAdminAccount', + 'macie2:GetMacieSession', + 'macie2:EnableMacie', + ], + Effect: 'Allow', + Resource: '*', + Sid: 'MacieEnableOrganizationAdminAccountTaskMacieActions', + }, + { + Action: ['iam:CreateServiceLinkedRole'], + Condition: { + StringLikeIfExists: { + 'iam:CreateServiceLinkedRole': ['macie.amazonaws.com'], + }, + }, + Effect: 'Allow', + Resource: '*', + Sid: 'MacieEnableMacieTaskIamAction', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * MacieEnableOrganizationAdminAccount custom resource configuration test + */ + test(`${testNamePrefix} MacieEnableOrganizationAdminAccount custom resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + MacieOrganizationAdminAccount2C23317B: { + Type: 'Custom::MacieEnableOrganizationAdminAccount', + DeletionPolicy: 'Delete', + UpdateReplacePolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomMacieEnableOrganizationAdminAccountCustomResourceProviderHandlerD7A9976A', 'Arn'], + }, + adminAccountId: { + Ref: 'AWS::AccountId', + }, + region: { + Ref: 'AWS::Region', + }, + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-macie/macie-session.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-macie/macie-session.test.ts new file mode 100644 index 000000000..8fa78cec6 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-macie/macie-session.test.ts @@ -0,0 +1,170 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { MacieSession } from '../../index'; + +const testNamePrefix = 'Construct(MacieSession): '; + +//Initialize stack for snapshot test and resource configuration test +const stack = new cdk.Stack(); + +new MacieSession(stack, 'MacieSession', { + isSensitiveSh: true, + findingPublishingFrequency: 'FIFTEEN_MINUTES', + logRetentionInDays: 3653, + kmsKey: new cdk.aws_kms.Key(stack, 'Key', {}), +}); +/** + * MacieSession construct test + */ +describe('MacieSession', () => { + /** + * Number of IAM role resource test + */ + test(`${testNamePrefix} IAM role resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 1); + }); + + /** + * Number of Lambda function resource test + */ + test(`${testNamePrefix} Lambda function resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 1); + }); + + /** + * Number of MacieEnableMacie custom resource test + */ + test(`${testNamePrefix} MacieEnableMacie custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::MacieEnableMacie', 1); + }); + + /** + * Lambda Function resource configuration test + */ + test(`${testNamePrefix} Lambda Function resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomMacieEnableMacieCustomResourceProviderHandler1B3444A0: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomMacieEnableMacieCustomResourceProviderRole2B29C97C'], + Properties: { + Code: { + S3Bucket: { + 'Fn::Sub': 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', + }, + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomMacieEnableMacieCustomResourceProviderRole2B29C97C', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * IAM role resource configuration test + */ + test(`${testNamePrefix} IAM role resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomMacieEnableMacieCustomResourceProviderRole2B29C97C: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: [ + 'macie2:DisableMacie', + 'macie2:EnableMacie', + 'macie2:GetMacieSession', + 'macie2:PutFindingsPublicationConfiguration', + 'macie2:UpdateMacieSession', + ], + Effect: 'Allow', + Resource: '*', + Sid: 'MacieEnableMacieTaskMacieActions', + }, + { + Action: ['iam:CreateServiceLinkedRole'], + Condition: { + StringLikeIfExists: { + 'iam:CreateServiceLinkedRole': ['macie.amazonaws.com'], + }, + }, + Effect: 'Allow', + Resource: '*', + Sid: 'MacieEnableMacieTaskIamAction', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * MacieEnableMacie custom resource configuration test + */ + test(`${testNamePrefix} MacieEnableMacie custom resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + MacieSession011BCE74: { + Type: 'Custom::MacieEnableMacie', + DeletionPolicy: 'Delete', + UpdateReplacePolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomMacieEnableMacieCustomResourceProviderHandler1B3444A0', 'Arn'], + }, + findingPublishingFrequency: 'FIFTEEN_MINUTES', + isSensitiveSh: true, + region: { + Ref: 'AWS::Region', + }, + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-networkfirewall/firewall.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-networkfirewall/firewall.test.ts new file mode 100644 index 000000000..6d2fefcbf --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-networkfirewall/firewall.test.ts @@ -0,0 +1,75 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; + +import { NetworkFirewall } from '../../lib/aws-networkfirewall/firewall'; + +const testNamePrefix = 'Construct(NetworkFirewall): '; + +//Initialize stack for resource configuration test +const stack = new cdk.Stack(); + +const firewallPolicyArn = 'arn:aws:network-firewall:us-east-1:222222222222:firewall-policy/TestPolicy'; + +new NetworkFirewall(stack, 'TestFirewall', { + firewallPolicyArn: firewallPolicyArn, + name: 'TestFirewall', + subnets: ['Test-Subnet-1', 'Test-Subnet-2'], + vpcId: 'TestVpc', + tags: [], +}); + +/** + * Network Firewall construct test + */ +describe('Network Firewall', () => { + /** + * Number of Network Firewalls test + */ + test(`${testNamePrefix} Network firewall count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::NetworkFirewall::Firewall', 1); + }); + + /** + * Network firewall resource configuration test + */ + test(`${testNamePrefix} Network firewall resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TestFirewallE26FCA5C: { + Type: 'AWS::NetworkFirewall::Firewall', + Properties: { + FirewallName: 'TestFirewall', + FirewallPolicyArn: 'arn:aws:network-firewall:us-east-1:222222222222:firewall-policy/TestPolicy', + SubnetMappings: [ + { + SubnetId: 'Test-Subnet-1', + }, + { + SubnetId: 'Test-Subnet-2', + }, + ], + VpcId: 'TestVpc', + Tags: [ + { + Key: 'Name', + Value: 'TestFirewall', + }, + ], + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-networkfirewall/get-network-firewall-endpoint.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-networkfirewall/get-network-firewall-endpoint.test.ts new file mode 100644 index 000000000..d728f87b0 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-networkfirewall/get-network-firewall-endpoint.test.ts @@ -0,0 +1,180 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; + +import { GetNetworkFirewallEndpoint } from '../../lib/aws-networkfirewall/get-network-firewall-endpoint'; + +const testNamePrefix = 'Construct(GetNetworkFirewallEndpoint): '; + +//Initialize stack for resource configuration test +const stack = new cdk.Stack(); + +const firewallArn = 'arn:aws:network-firewall:us-east-1:222222222222:firewall/TestFirewall'; + +new GetNetworkFirewallEndpoint(stack, 'TestGetEndpoint', { + endpointAz: 'us-east-1a', + firewallArn: firewallArn, + kmsKey: new cdk.aws_kms.Key(stack, 'Custom', {}), + logRetentionInDays: 3653, + region: 'us-east-1', +}); + +/** + * Get Network Firewall endpoint construct test + */ +describe('Get Network Firewall endpoint', () => { + /** + * Number of Network Firewall endpoint custom resource + */ + test(`${testNamePrefix} Network firewall count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::GetNetworkFirewallEndpoint', 1); + }); + + /** + * Number of IAM roles + */ + test(`${testNamePrefix} IAM role count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 1); + }); + + /** + * Number of Lambda functions + */ + test(`${testNamePrefix} Lambda function count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 1); + }); + + /** + * Number of log groups + */ + test(`${testNamePrefix} CloudWatch log group count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Logs::LogGroup', 1); + }); + + /** + * Get network firewall endpoint resource configuration test + */ + test(`${testNamePrefix} Network firewall policy resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TestGetEndpoint7804FE92: { + Type: 'Custom::GetNetworkFirewallEndpoint', + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomGetNetworkFirewallEndpointCustomResourceProviderHandler2EF030A1', 'Arn'], + }, + endpointAz: 'us-east-1a', + firewallArn: 'arn:aws:network-firewall:us-east-1:222222222222:firewall/TestFirewall', + region: 'us-east-1', + }, + }, + }, + }); + }); + + /** + * Get network firewall endpoint resource configuration test + */ + test(`${testNamePrefix} IAM role resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomGetNetworkFirewallEndpointCustomResourceProviderRole540B9917: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: ['network-firewall:DescribeFirewall'], + Effect: 'Allow', + Resource: '*', + }, + ], + }, + }, + ], + }, + }, + }, + }); + }); + + /** + * Lambda function resource config test + */ + test(`${testNamePrefix} Lambda function resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomGetNetworkFirewallEndpointCustomResourceProviderHandler2EF030A1: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: { + 'Fn::Sub': 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', + }, + S3Key: cdk.assertions.Match.stringLikeRegexp('\\w+.zip'), + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomGetNetworkFirewallEndpointCustomResourceProviderRole540B9917', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * CloudWatch log group resource config test + */ + test(`${testNamePrefix} CloudWatch Log Group resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomGetNetworkFirewallEndpointCustomResourceProviderLogGroup98AC3B14: { + Type: 'AWS::Logs::LogGroup', + Properties: { + KmsKeyId: { 'Fn::GetAtt': ['Custom8166710A', 'Arn'] }, + LogGroupName: { + 'Fn::Join': [ + '', + ['/aws/lambda/', { Ref: 'CustomGetNetworkFirewallEndpointCustomResourceProviderHandler2EF030A1' }], + ], + }, + RetentionInDays: 3653, + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-networkfirewall/policy.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-networkfirewall/policy.test.ts new file mode 100644 index 000000000..20ebd4f32 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-networkfirewall/policy.test.ts @@ -0,0 +1,81 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; + +import { FirewallPolicyProperty, NetworkFirewallPolicy } from '../../lib/aws-networkfirewall/policy'; + +const testNamePrefix = 'Construct(NetworkFirewallPolicy): '; + +//Initialize stack for resource configuration test +const stack = new cdk.Stack(); + +const firewallPolicy: FirewallPolicyProperty = { + statelessDefaultActions: ['aws:forward_to_sfe'], + statelessFragmentDefaultActions: ['aws:forward_to_sfe'], + statefulEngineOptions: 'STRICT_ORDER', + statefulRuleGroupReferences: [ + { + priority: 123, + resourceArn: 'arn:aws:network-firewall:us-east-1:222222222222:stateful-rulegroup/TestGroup', + }, + ], +}; + +new NetworkFirewallPolicy(stack, 'TestPolicy', { + firewallPolicy: firewallPolicy, + name: 'TestFirewallPolicy', + tags: [], +}); + +/** + * Network Firewall construct test + */ +describe('Network Firewall Policy', () => { + /** + * Number of Network Firewall Policy test + */ + test(`${testNamePrefix} Network firewall count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::NetworkFirewall::FirewallPolicy', 1); + }); + + /** + * Network firewall resource configuration test + */ + test(`${testNamePrefix} Network firewall policy resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TestPolicyCC05E598: { + Type: 'AWS::NetworkFirewall::FirewallPolicy', + Properties: { + FirewallPolicy: { + StatefulEngineOptions: { + RuleOrder: 'STRICT_ORDER', + }, + StatefulRuleGroupReferences: [ + { + Priority: 123, + ResourceArn: 'arn:aws:network-firewall:us-east-1:222222222222:stateful-rulegroup/TestGroup', + }, + ], + StatelessDefaultActions: ['aws:forward_to_sfe'], + StatelessFragmentDefaultActions: ['aws:forward_to_sfe'], + }, + FirewallPolicyName: 'TestFirewallPolicy', + Tags: [{ Key: 'Name', Value: 'TestFirewallPolicy' }], + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-networkfirewall/rule-group.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-networkfirewall/rule-group.test.ts new file mode 100644 index 000000000..754b07bf1 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-networkfirewall/rule-group.test.ts @@ -0,0 +1,122 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; + +import { NfwRuleGroupRuleConfig } from '@aws-accelerator/config'; + +import { NetworkFirewallRuleGroup } from '../../lib/aws-networkfirewall/rule-group'; + +const testNamePrefix = 'Construct(NetworkFirewallPolicy): '; + +//Initialize stack for resource configuration test +const stack = new cdk.Stack(); + +const ruleGroup: NfwRuleGroupRuleConfig = { + rulesSource: { + statefulRules: [ + { + action: 'PASS', + header: { + destination: '10.0.0.0/16', + destinationPort: 'ANY', + direction: 'FORWARD', + protocol: 'IP', + source: '10.1.0.0/16', + sourcePort: 'ANY', + }, + ruleOptions: [ + { + keyword: 'sid', + settings: ['100'], + }, + ], + }, + ], + rulesSourceList: undefined, + statelessRulesAndCustomActions: undefined, + rulesString: undefined, + }, + ruleVariables: undefined, + statefulRuleOptions: 'STRICT_ORDER', +}; + +new NetworkFirewallRuleGroup(stack, 'TestGroup', { + name: 'TestGroup', + capacity: 100, + type: 'STATEFUL', + ruleGroup: ruleGroup, + tags: [], +}); + +/** + * Network Firewall construct test + */ +describe('Network Firewall Rule Group', () => { + /** + * Number of Network Firewall Policy test + */ + test(`${testNamePrefix} Network firewall rule group count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::NetworkFirewall::RuleGroup', 1); + }); + + /** + * Network firewall resource configuration test + */ + test(`${testNamePrefix} Network firewall policy resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TestGroupAF88660E: { + Type: 'AWS::NetworkFirewall::RuleGroup', + Properties: { + Capacity: 100, + RuleGroup: { + RulesSource: { + StatefulRules: [ + { + Action: 'PASS', + Header: { + Destination: '10.0.0.0/16', + DestinationPort: 'ANY', + Direction: 'FORWARD', + Protocol: 'IP', + Source: '10.1.0.0/16', + SourcePort: 'ANY', + }, + RuleOptions: [ + { + Keyword: 'sid', + Settings: ['100'], + }, + ], + }, + ], + }, + StatefulRuleOptions: { + RuleOrder: 'STRICT_ORDER', + }, + }, + RuleGroupName: 'TestGroup', + Tags: [ + { + Key: 'Name', + Value: 'TestGroup', + }, + ], + Type: 'STATEFUL', + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-organizations/account.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-organizations/account.test.ts new file mode 100644 index 000000000..1f5384a77 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-organizations/account.test.ts @@ -0,0 +1,173 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Account } from '../../index'; + +const testNamePrefix = 'Construct(Account): '; + +//Initialize stack for snapshot test and resource configuration test +const stack = new cdk.Stack(); + +new Account(stack, 'Account', { + accountId: stack.account, + assumeRoleName: 'AWSControlTowerExecution', + kmsKey: new cdk.aws_kms.Key(stack, 'CustomKey', {}), + logRetentionInDays: 3653, +}); + +/** + * Account construct test + */ +describe('Account', () => { + /** + * Number of IAM role resource test + */ + test(`${testNamePrefix} IAM role resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 1); + }); + + /** + * Number of Lambda function resource test + */ + test(`${testNamePrefix} Lambda function resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 1); + }); + + /** + * Number of InviteAccountToOrganization custom resource test + */ + test(`${testNamePrefix} InviteAccountToOrganization custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::InviteAccountToOrganization', 1); + }); + + /** + * InviteAccountToOrganization custom resource configuration test + */ + test(`${testNamePrefix} InviteAccountToOrganization custom resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + Account0D856946: { + Type: 'Custom::InviteAccountToOrganization', + UpdateReplacePolicy: 'Delete', + DeletionPolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomInviteAccountToOrganizationCustomResourceProviderHandlerAEB26818', 'Arn'], + }, + accountId: { + Ref: 'AWS::AccountId', + }, + roleArn: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::', + { + Ref: 'AWS::AccountId', + }, + ':role/AWSControlTowerExecution', + ], + ], + }, + }, + }, + }, + }); + }); + + /** + * Lambda Function resource configuration test + */ + test(`${testNamePrefix} Lambda Function resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomInviteAccountToOrganizationCustomResourceProviderHandlerAEB26818: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomInviteAccountToOrganizationCustomResourceProviderRole0F64F419'], + Properties: { + Code: { + S3Bucket: { + 'Fn::Sub': 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', + }, + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomInviteAccountToOrganizationCustomResourceProviderRole0F64F419', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * Lambda Function resource configuration test + */ + test(`${testNamePrefix} Lambda Function resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomInviteAccountToOrganizationCustomResourceProviderRole0F64F419: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: [ + 'organizations:AcceptHandshake', + 'organizations:ListAccounts', + 'organizations:InviteAccountToOrganization', + 'organizations:MoveAccount', + 'sts:AssumeRole', + ], + Effect: 'Allow', + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-organizations/enable-aws-service-access.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-organizations/enable-aws-service-access.test.ts new file mode 100644 index 000000000..c3b3fd332 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-organizations/enable-aws-service-access.test.ts @@ -0,0 +1,148 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { EnableAwsServiceAccess } from '../../index'; + +const testNamePrefix = 'Construct(EnableAwsServiceAccess): '; + +//Initialize stack for snapshot test and resource configuration test +const stack = new cdk.Stack(); + +new EnableAwsServiceAccess(stack, 'EnableAwsServiceAccess', { + servicePrincipal: 's3.amazonaws.com', + kmsKey: new cdk.aws_kms.Key(stack, 'CustomKey', {}), + logRetentionInDays: 3653, +}); + +/** + * EnableAwsServiceAccess construct test + */ +describe('EnableAwsServiceAccess', () => { + /** + * Number of IAM role resource test + */ + test(`${testNamePrefix} IAM role resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 1); + }); + + /** + * Number of Lambda function resource test + */ + test(`${testNamePrefix} Lambda function resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 1); + }); + + /** + * Number of EnableAwsServiceAccess custom resource test + */ + test(`${testNamePrefix} EnableAwsServiceAccess custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::EnableAwsServiceAccess', 1); + }); + + /** + * Lambda Function resource configuration test + */ + test(`${testNamePrefix} Lambda Function resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomOrganizationsEnableAwsServiceAccessCustomResourceProviderHandlerDCD56D71: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomOrganizationsEnableAwsServiceAccessCustomResourceProviderRole59F76BA2'], + Properties: { + Code: { + S3Bucket: { + 'Fn::Sub': 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', + }, + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomOrganizationsEnableAwsServiceAccessCustomResourceProviderRole59F76BA2', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * IAM role resource configuration test + */ + test(`${testNamePrefix} IAM role resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomOrganizationsEnableAwsServiceAccessCustomResourceProviderRole59F76BA2: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: ['organizations:DisableAWSServiceAccess', 'organizations:EnableAwsServiceAccess'], + Effect: 'Allow', + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * EnableAwsServiceAccess custom resource configuration test + */ + test(`${testNamePrefix} EnableAwsServiceAccess custom resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + EnableAwsServiceAccessFCD8AE04: { + Type: 'Custom::EnableAwsServiceAccess', + UpdateReplacePolicy: 'Delete', + DeletionPolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomOrganizationsEnableAwsServiceAccessCustomResourceProviderHandlerDCD56D71', 'Arn'], + }, + servicePrincipal: 's3.amazonaws.com', + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-organizations/enable-policy-type.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-organizations/enable-policy-type.test.ts new file mode 100644 index 000000000..c6aed4b0b --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-organizations/enable-policy-type.test.ts @@ -0,0 +1,173 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { EnablePolicyType, PolicyTypeEnum } from '../../index'; + +const testNamePrefix = 'Construct(EnablePolicyType): '; + +//Initialize stack for snapshot test and resource configuration test +const stack = new cdk.Stack(); + +new EnablePolicyType(stack, 'EnablePolicyType', { + policyType: PolicyTypeEnum.SERVICE_CONTROL_POLICY, + kmsKey: new cdk.aws_kms.Key(stack, 'CustomKey', {}), + logRetentionInDays: 3653, +}); +/** + * EnablePolicyType construct test + */ +describe('EnablePolicyType', () => { + /** + * Number of IAM role resource test + */ + test(`${testNamePrefix} IAM role resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 1); + }); + + /** + * Number of Lambda function resource test + */ + test(`${testNamePrefix} Lambda function resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 1); + }); + + /** + * Number of EnablePolicyType custom resource test + */ + test(`${testNamePrefix} EnablePolicyType custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::EnablePolicyType', 1); + }); + + /** + * Lambda Function resource configuration test + */ + test(`${testNamePrefix} Lambda Function resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomEnablePolicyTypeCustomResourceProviderHandlerC244F9E1: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomEnablePolicyTypeCustomResourceProviderRoleAE71B2CA'], + Properties: { + Code: { + S3Bucket: { + 'Fn::Sub': 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', + }, + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomEnablePolicyTypeCustomResourceProviderRoleAE71B2CA', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * IAM role resource configuration test + */ + test(`${testNamePrefix} IAM role resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomEnablePolicyTypeCustomResourceProviderRoleAE71B2CA: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: [ + 'organizations:DescribeOrganization', + 'organizations:DisablePolicyType', + 'organizations:EnablePolicyType', + 'organizations:ListRoots', + 'organizations:ListPoliciesForTarget', + 'organizations:ListTargetsForPolicy', + 'organizations:DescribeEffectivePolicy', + 'organizations:DescribePolicy', + 'organizations:DisableAWSServiceAccess', + 'organizations:DetachPolicy', + 'organizations:DeletePolicy', + 'organizations:DescribeAccount', + 'organizations:ListAWSServiceAccessForOrganization', + 'organizations:ListPolicies', + 'organizations:ListAccountsForParent', + 'organizations:ListAccounts', + 'organizations:EnableAWSServiceAccess', + 'organizations:ListCreateAccountStatus', + 'organizations:UpdatePolicy', + 'organizations:DescribeOrganizationalUnit', + 'organizations:AttachPolicy', + 'organizations:ListParents', + 'organizations:ListOrganizationalUnitsForParent', + 'organizations:CreatePolicy', + 'organizations:DescribeCreateAccountStatus', + ], + Effect: 'Allow', + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * EnablePolicyType custom resource configuration test + */ + test(`${testNamePrefix} EnablePolicyType custom resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + EnablePolicyTypeA517D946: { + Type: 'Custom::EnablePolicyType', + DeletionPolicy: 'Delete', + UpdateReplacePolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomEnablePolicyTypeCustomResourceProviderHandlerC244F9E1', 'Arn'], + }, + policyType: 'SERVICE_CONTROL_POLICY', + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-organizations/organization.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-organizations/organization.test.ts new file mode 100644 index 000000000..ba56f30dc --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-organizations/organization.test.ts @@ -0,0 +1,143 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Organization } from '../../index'; + +const testNamePrefix = 'Construct(Organization): '; + +//Initialize stack for snapshot test and resource configuration test +const stack = new cdk.Stack(); + +new Organization(stack, 'Organization'); + +/** + * Organization construct test + */ +describe('Organization', () => { + /** + * Number of IAM role resource test + */ + test(`${testNamePrefix} IAM role resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 1); + }); + + /** + * Number of Lambda function resource test + */ + test(`${testNamePrefix} Lambda function resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 1); + }); + + /** + * Number of DescribeOrganization custom resource test + */ + test(`${testNamePrefix} DescribeOrganization custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::DescribeOrganization', 1); + }); + + /** + * Lambda Function resource configuration test + */ + test(`${testNamePrefix} Lambda Function resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomOrganizationsDescribeOrganizationCustomResourceProviderHandler4C6F49D1: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomOrganizationsDescribeOrganizationCustomResourceProviderRole775854D5'], + Properties: { + Code: { + S3Bucket: { + 'Fn::Sub': 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', + }, + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomOrganizationsDescribeOrganizationCustomResourceProviderRole775854D5', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * IAM role resource configuration test + */ + test(`${testNamePrefix} IAM role resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomOrganizationsDescribeOrganizationCustomResourceProviderRole775854D5: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: ['organizations:DescribeOrganization'], + Effect: 'Allow', + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * DescribeOrganization custom resource configuration test + */ + test(`${testNamePrefix} DescribeOrganization custom resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + Organization29A5FC3F: { + Type: 'Custom::DescribeOrganization', + DeletionPolicy: 'Delete', + UpdateReplacePolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomOrganizationsDescribeOrganizationCustomResourceProviderHandler4C6F49D1', 'Arn'], + }, + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-organizations/organizational-units.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-organizations/organizational-units.test.ts new file mode 100644 index 000000000..913de2822 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-organizations/organizational-units.test.ts @@ -0,0 +1,177 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { OrganizationalUnits } from '../../index'; +//import { SynthUtils } from '@aws-cdk/assert'; + +const testNamePrefix = 'Construct(OrganizationalUnit): '; + +//Initialize stack for snapshot test and resource configuration test +const stack = new cdk.Stack(); + +new OrganizationalUnits(stack, 'OrganizationalUnits', { + acceleratorConfigTable: new cdk.aws_dynamodb.Table(stack, 'ConfigTable', { + partitionKey: { name: 'dataType', type: cdk.aws_dynamodb.AttributeType.STRING }, + }), + commitId: 'bda32a39', + controlTowerEnabled: true, + organizationsEnabled: true, + kmsKey: new cdk.aws_kms.Key(stack, 'CustomKey', {}), + logRetentionInDays: 365, +}); + +/** + * OrganizationalUnit construct test + */ +describe('OrganizationalUnits', () => { + // test(`${testNamePrefix} Snapshot Test`, () => { + // expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); + // }); + + /** + * Number of IAM role resource test + */ + test(`${testNamePrefix} IAM role resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 1); + }); + + /** + * Number of Lambda function resource test + */ + test(`${testNamePrefix} Lambda function resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 1); + }); + + /** + * Number of CreateOrganizationalUnit custom resource test + */ + test(`${testNamePrefix} CreateOrganizationalUnits custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::CreateOrganizationalUnits', 1); + }); + + /** + * Lambda Function resource configuration test + */ + test(`${testNamePrefix} Lambda Function resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomOrganizationsCreateOrganizationalUnitsCustomResourceProviderHandler4596F0BC: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomOrganizationsCreateOrganizationalUnitsCustomResourceProviderRole4B8B81B0'], + Properties: { + Code: { + S3Bucket: { + 'Fn::Sub': 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', + }, + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomOrganizationsCreateOrganizationalUnitsCustomResourceProviderRole4B8B81B0', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * IAM role resource configuration test + */ + test(`${testNamePrefix} IAM role resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomOrganizationsCreateOrganizationalUnitsCustomResourceProviderRole4B8B81B0: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: [ + 'organizations:CreateOrganizationalUnit', + 'organizations:ListOrganizationalUnitsForParent', + 'organizations:ListRoots', + 'organizations:UpdateOrganizationalUnit', + ], + Effect: 'Allow', + Resource: '*', + Sid: 'organizations', + }, + { + Action: ['dynamodb:UpdateItem', 'dynamodb:Query'], + Effect: 'Allow', + Resource: [ + { + 'Fn::GetAtt': ['ConfigTable5CD72349', 'Arn'], + }, + ], + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * CreateOrganizationalUnit custom resource configuration test + */ + test(`${testNamePrefix} CreateOrganizationalUnits custom resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + OrganizationalUnits30245726: { + Type: 'Custom::CreateOrganizationalUnits', + DeletionPolicy: 'Delete', + UpdateReplacePolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': [ + 'CustomOrganizationsCreateOrganizationalUnitsCustomResourceProviderHandler4596F0BC', + 'Arn', + ], + }, + configTableName: { Ref: 'ConfigTable5CD72349' }, + partition: { Ref: 'AWS::Partition' }, + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-organizations/policy-attachment.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-organizations/policy-attachment.test.ts new file mode 100644 index 000000000..2082d540b --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-organizations/policy-attachment.test.ts @@ -0,0 +1,156 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { PolicyAttachment, PolicyType } from '../../index'; + +const testNamePrefix = 'Construct(PolicyAttachment): '; + +//Initialize stack for snapshot test and resource configuration test +const stack = new cdk.Stack(); + +new PolicyAttachment(stack, 'PolicyAttachment', { + policyId: 'policyId', + targetId: 'targetId', + type: PolicyType.SERVICE_CONTROL_POLICY, + kmsKey: new cdk.aws_kms.Key(stack, 'CustomKey', {}), + logRetentionInDays: 3653, +}); + +/** + * PolicyAttachment construct test + */ +describe('PolicyAttachment', () => { + /** + * Number of IAM role resource test + */ + test(`${testNamePrefix} IAM role resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 1); + }); + + /** + * Number of Lambda function resource test + */ + test(`${testNamePrefix} Lambda function resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 1); + }); + + /** + * Number of AttachPolicy custom resource test + */ + test(`${testNamePrefix} AttachPolicy custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::AttachPolicy', 1); + }); + + /** + * Lambda Function resource configuration test + */ + test(`${testNamePrefix} Lambda Function resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomOrganizationsAttachPolicyCustomResourceProviderHandlerB3233202: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomOrganizationsAttachPolicyCustomResourceProviderRole051E00A6'], + Properties: { + Code: { + S3Bucket: { + 'Fn::Sub': 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', + }, + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomOrganizationsAttachPolicyCustomResourceProviderRole051E00A6', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * IAM role resource configuration test + */ + test(`${testNamePrefix} IAM role resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomOrganizationsAttachPolicyCustomResourceProviderRole051E00A6: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: [ + 'organizations:AttachPolicy', + 'organizations:DetachPolicy', + 'organizations:ListPoliciesForTarget', + ], + Effect: 'Allow', + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * AttachPolicy custom resource configuration test + */ + test(`${testNamePrefix} AttachPolicy custom resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + PolicyAttachmentE9E858C2: { + Type: 'Custom::AttachPolicy', + DeletionPolicy: 'Delete', + UpdateReplacePolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomOrganizationsAttachPolicyCustomResourceProviderHandlerB3233202', 'Arn'], + }, + policyId: 'policyId', + targetId: 'targetId', + type: 'SERVICE_CONTROL_POLICY', + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-organizations/policy.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-organizations/policy.test.ts new file mode 100644 index 000000000..ac672eb56 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-organizations/policy.test.ts @@ -0,0 +1,196 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { Policy, PolicyType } from '../../index'; + +const testNamePrefix = 'Construct(Policy): '; + +//Initialize stack for snapshot test and resource configuration test +const stack = new cdk.Stack(); + +new Policy(stack, 'Policy', { + path: __dirname, + name: 'TestPolicy', + description: 'Testing Policy construct', + type: PolicyType.SERVICE_CONTROL_POLICY, + tags: [ + { Key: 'name', Value: 'TestPolicy' }, + { Key: 'usage', Value: 'ConstructTest' }, + ], + kmsKey: new cdk.aws_kms.Key(stack, 'CustomKey', {}), + logRetentionInDays: 3653, + acceleratorPrefix: 'AWSAccelerator', + managementAccountAccessRole: 'AWSControlTowerExecution', +}); + +/** + * Policy construct test + */ +describe('Policy', () => { + /** + * Number of IAM role resource test + */ + test(`${testNamePrefix} IAM role resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 1); + }); + + /** + * Number of Lambda function resource test + */ + test(`${testNamePrefix} Lambda function resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 1); + }); + + /** + * Number of CreatePolicy custom resource test + */ + test(`${testNamePrefix} CreatePolicy custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::CreatePolicy', 1); + }); + + /** + * Lambda Function resource configuration test + */ + test(`${testNamePrefix} Lambda Function resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomOrganizationsCreatePolicyCustomResourceProviderHandler7A188619: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomOrganizationsCreatePolicyCustomResourceProviderRoleBA0ADB43'], + Properties: { + Code: { + S3Bucket: { + 'Fn::Sub': 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', + }, + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomOrganizationsCreatePolicyCustomResourceProviderRoleBA0ADB43', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * IAM role resource configuration test + */ + test(`${testNamePrefix} IAM role resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomOrganizationsCreatePolicyCustomResourceProviderRoleBA0ADB43: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: [ + 'organizations:CreatePolicy', + 'organizations:ListPolicies', + 'organizations:UpdatePolicy', + ], + Effect: 'Allow', + Resource: '*', + }, + { + Action: ['s3:GetObject'], + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':s3:::', + { + 'Fn::Sub': 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', + }, + '/*', + ], + ], + }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * CreatePolicy custom resource configuration test + */ + test(`${testNamePrefix} CreatePolicy custom resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + Policy23B91518: { + Type: 'Custom::CreatePolicy', + DeletionPolicy: 'Delete', + UpdateReplacePolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomOrganizationsCreatePolicyCustomResourceProviderHandler7A188619', 'Arn'], + }, + bucket: { + 'Fn::Sub': 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', + }, + description: 'Testing Policy construct', + name: 'TestPolicy', + tags: [ + { + Key: 'name', + Value: 'TestPolicy', + }, + { + Key: 'usage', + Value: 'ConstructTest', + }, + ], + type: 'SERVICE_CONTROL_POLICY', + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-organizations/register-delegated-administrator.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-organizations/register-delegated-administrator.test.ts new file mode 100644 index 000000000..41a769d40 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-organizations/register-delegated-administrator.test.ts @@ -0,0 +1,161 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { RegisterDelegatedAdministrator } from '../../index'; + +const testNamePrefix = 'Construct(RegisterDelegatedAdministrator): '; + +//Initialize stack for snapshot test and resource configuration test +const stack = new cdk.Stack(); + +new RegisterDelegatedAdministrator(stack, 'RegisterDelegatedAdministrator', { + servicePrincipal: 'macie.amazonaws.com', + accountId: stack.account, + kmsKey: new cdk.aws_kms.Key(stack, 'CustomKey', {}), + logRetentionInDays: 3653, +}); + +/** + * RegisterDelegatedAdministrator construct test + */ +describe('RegisterDelegatedAdministrator', () => { + /** + * Number of IAM role resource test + */ + test(`${testNamePrefix} IAM role resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 1); + }); + + /** + * Number of Lambda function resource test + */ + test(`${testNamePrefix} Lambda function resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 1); + }); + + /** + * Number of OrganizationsRegisterDelegatedAdministrator custom resource test + */ + test(`${testNamePrefix} EnablePolicyType custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::OrganizationsRegisterDelegatedAdministrator', 1); + }); + + /** + * Lambda Function resource configuration test + */ + test(`${testNamePrefix} Lambda Function resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomOrganizationsRegisterDelegatedAdministratorCustomResourceProviderHandlerFAEA655C: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomOrganizationsRegisterDelegatedAdministratorCustomResourceProviderRole4B3EAD1B'], + Properties: { + Code: { + S3Bucket: { + 'Fn::Sub': 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', + }, + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': [ + 'CustomOrganizationsRegisterDelegatedAdministratorCustomResourceProviderRole4B3EAD1B', + 'Arn', + ], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * IAM role resource configuration test + */ + test(`${testNamePrefix} IAM role resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomOrganizationsRegisterDelegatedAdministratorCustomResourceProviderRole4B3EAD1B: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: [ + 'organizations:DeregisterDelegatedAdministrator', + 'organizations:RegisterDelegatedAdministrator', + ], + Effect: 'Allow', + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * OrganizationsRegisterDelegatedAdministrator custom resource configuration test + */ + test(`${testNamePrefix} OrganizationsRegisterDelegatedAdministrator custom resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + RegisterDelegatedAdministratorF9498A1E: { + Type: 'Custom::OrganizationsRegisterDelegatedAdministrator', + DeletionPolicy: 'Delete', + UpdateReplacePolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': [ + 'CustomOrganizationsRegisterDelegatedAdministratorCustomResourceProviderHandlerFAEA655C', + 'Arn', + ], + }, + accountId: { + Ref: 'AWS::AccountId', + }, + servicePrincipal: 'macie.amazonaws.com', + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-ram/__snapshots__/resource-share.test.ts.snap b/source/packages/@aws-accelerator/constructs/test/aws-ram/__snapshots__/resource-share.test.ts.snap new file mode 100644 index 000000000..94b57b22c --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-ram/__snapshots__/resource-share.test.ts.snap @@ -0,0 +1,62 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ResourceShare Construct(ResourceShare): Snapshot Test 1`] = ` +Object { + "Resources": Object { + "ResourceShareTestResourceShareResourceShare8D7B67C7": Object { + "Properties": Object { + "AllowExternalPrincipals": true, + "Name": "TestResourceShare", + "PermissionArns": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":s3:::test-bucket-1-", + Object { + "Ref": "AWS::AccountId", + }, + "-", + Object { + "Ref": "AWS::Region", + }, + ], + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":s3:::test-bucket-2-", + Object { + "Ref": "AWS::AccountId", + }, + "-", + Object { + "Ref": "AWS::Region", + }, + ], + ], + }, + ], + "Principals": Array [ + "accountID", + "organizationUnitId", + ], + "ResourceArns": Array [ + "ec2:TransitGateway", + ], + }, + "Type": "AWS::RAM::ResourceShare", + }, + }, +} +`; diff --git a/source/packages/@aws-accelerator/constructs/test/aws-ram/enable-sharing-with-aws-organization.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-ram/enable-sharing-with-aws-organization.test.ts new file mode 100644 index 000000000..8306d65fc --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-ram/enable-sharing-with-aws-organization.test.ts @@ -0,0 +1,152 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { EnableSharingWithAwsOrganization } from '../../index'; + +const testNamePrefix = 'Construct(EnableSharingWithAwsOrganization): '; + +//Initialize stack for snapshot test and resource configuration test +const stack = new cdk.Stack(); + +new EnableSharingWithAwsOrganization(stack, 'EnableSharingWithAwsOrganization', { + kmsKey: new cdk.aws_kms.Key(stack, 'CustomKey', {}), + logRetentionInDays: 3653, +}); + +/** + * HostedZone construct test + */ +describe('EnableSharingWithAwsOrganization', () => { + /** + * Number of Lambda function test + */ + test(`${testNamePrefix} Lambda function count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 1); + }); + + /** + * Number of IAM role test + */ + test(`${testNamePrefix} IAM role count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 1); + }); + + /** + * Number of EnableSharingWithAwsOrganization custom resource test + */ + test(`${testNamePrefix} EnableSharingWithAwsOrganization custom resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::EnableSharingWithAwsOrganization', 1); + }); + + /** + * Lambda function resource configuration test + */ + test(`${testNamePrefix} Lambda function resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomEnableSharingWithAwsOrganizationCustomResourceProviderHandler405D7398: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomEnableSharingWithAwsOrganizationCustomResourceProviderRole4FE5EBD7'], + Properties: { + Code: { + S3Bucket: { + 'Fn::Sub': 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', + }, + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomEnableSharingWithAwsOrganizationCustomResourceProviderRole4FE5EBD7', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * IAM role resource configuration test + */ + test(`${testNamePrefix} IAM role resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomEnableSharingWithAwsOrganizationCustomResourceProviderRole4FE5EBD7: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: [ + 'ram:EnableSharingWithAwsOrganization', + 'iam:CreateServiceLinkedRole', + 'organizations:EnableAWSServiceAccess', + 'organizations:ListAWSServiceAccessForOrganization', + 'organizations:DescribeOrganization', + ], + Effect: 'Allow', + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * EnableSharingWithAwsOrganization custom resource configuration test + */ + test(`${testNamePrefix} EnableSharingWithAwsOrganization custom resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + EnableSharingWithAwsOrganization81D5714F: { + Type: 'Custom::EnableSharingWithAwsOrganization', + DeletionPolicy: 'Delete', + UpdateReplacePolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomEnableSharingWithAwsOrganizationCustomResourceProviderHandler405D7398', 'Arn'], + }, + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-ram/resource-share.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-ram/resource-share.test.ts new file mode 100644 index 000000000..d802a6bbb --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-ram/resource-share.test.ts @@ -0,0 +1,113 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { SynthUtils } from '@aws-cdk/assert'; +import { ResourceShare } from '../../index'; + +const testNamePrefix = 'Construct(ResourceShare): '; + +//Initialize stack for snapshot test and resource configuration test +const stack = new cdk.Stack(); + +new ResourceShare(stack, 'ResourceShare', { + name: 'TestResourceShare', + allowExternalPrincipals: true, + permissionArns: [ + `arn:${stack.partition}:s3:::test-bucket-1-${stack.account}-${stack.region}`, + `arn:${stack.partition}:s3:::test-bucket-2-${stack.account}-${stack.region}`, + ], + + principals: ['accountID', 'organizationUnitId'], + resourceArns: ['ec2:TransitGateway'], +}); + +/** + * ResourceShare construct test + */ +describe('ResourceShare', () => { + /** + * Snapshot test + */ + test(`${testNamePrefix} Snapshot Test`, () => { + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); + }); + + /** + * Number of ResourceShare resource test + */ + test(`${testNamePrefix} ResourceShare resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::RAM::ResourceShare', 1); + }); + + /** + * ResourceShare resource configuration test + */ + test(`${testNamePrefix} ResourceShare resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + ResourceShareTestResourceShareResourceShare8D7B67C7: { + Type: 'AWS::RAM::ResourceShare', + Properties: { + AllowExternalPrincipals: true, + Name: 'TestResourceShare', + PermissionArns: [ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':s3:::test-bucket-1-', + { + Ref: 'AWS::AccountId', + }, + '-', + { + Ref: 'AWS::Region', + }, + ], + ], + }, + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':s3:::test-bucket-2-', + { + Ref: 'AWS::AccountId', + }, + '-', + { + Ref: 'AWS::Region', + }, + ], + ], + }, + ], + Principals: ['accountID', 'organizationUnitId'], + ResourceArns: ['ec2:TransitGateway'], + }, + }, + }, + }); + }); + + //End of file +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-route-53-resolver/endpoint-addresses.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-route-53-resolver/endpoint-addresses.test.ts new file mode 100644 index 000000000..7d093f97a --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-route-53-resolver/endpoint-addresses.test.ts @@ -0,0 +1,146 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; + +import { EndpointAddresses } from '../../lib/aws-route-53-resolver/endpoint-addresses'; + +const testNamePrefix = 'Construct(ResolverEndpointAddresses): '; + +const stack = new cdk.Stack(); + +new EndpointAddresses(stack, 'TestEndpointAddresses', { + endpointId: 'TestEndpointId', + kmsKey: new cdk.aws_kms.Key(stack, 'CustomKey', {}), + logRetentionInDays: 3653, +}); + +/** + * Resolver endpoint addresses construct test + */ +describe('EndpointAddresses', () => { + /** + * Resolver endpoint addresses count test + */ + test(`${testNamePrefix} Resolver endpoint addresses count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::ResolverEndpointAddresses', 1); + }); + + /** + * IAM role count test + */ + test(`${testNamePrefix} IAM role count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 1); + }); + + /** + * Lambda function count test + */ + test(`${testNamePrefix} Lambda function count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 1); + }); + + /** + * Resolver endpoint addresses resource config test + */ + test(`${testNamePrefix} Resolver endpoint addresses resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TestEndpointAddressesCE4F1BB4: { + Type: 'Custom::ResolverEndpointAddresses', + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomResolverEndpointAddressesCustomResourceProviderHandler09D4123E', 'Arn'], + }, + endpointId: 'TestEndpointId', + region: { + Ref: 'AWS::Region', + }, + }, + }, + }, + }); + }); + + /** + * IAM role resource config test + */ + test(`${testNamePrefix} IAM role resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomResolverEndpointAddressesCustomResourceProviderRoleA94B4F27: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: ['route53resolver:ListResolverEndpointIpAddresses'], + Effect: 'Allow', + Resource: '*', + }, + ], + }, + }, + ], + }, + }, + }, + }); + }); + + /** + * Lambda function resource config test + */ + test(`${testNamePrefix} Lambda function resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomResolverEndpointAddressesCustomResourceProviderHandler09D4123E: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: { + 'Fn::Sub': 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', + }, + S3Key: cdk.assertions.Match.stringLikeRegexp('\\w+.zip'), + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomResolverEndpointAddressesCustomResourceProviderRoleA94B4F27', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-route-53-resolver/firewall-domain-list.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-route-53-resolver/firewall-domain-list.test.ts new file mode 100644 index 000000000..a6f90cdd8 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-route-53-resolver/firewall-domain-list.test.ts @@ -0,0 +1,194 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; + +import { + ResolverFirewallDomainList, + ResolverFirewallDomainListType, +} from '../../lib/aws-route-53-resolver/firewall-domain-list'; + +const testNamePrefix = 'Construct(ResolverFirewallDomainList): '; + +const stack = new cdk.Stack(); + +// Custom domain list +new ResolverFirewallDomainList(stack, 'TestDomainList', { + name: 'TestDomainList', + path: __dirname, + tags: [], + type: ResolverFirewallDomainListType.CUSTOM, + kmsKey: new cdk.aws_kms.Key(stack, 'TestDomainListKey', {}), + logRetentionInDays: 3653, +}); + +// Managed domain list +new ResolverFirewallDomainList(stack, 'TestManagedDomainList', { + name: 'TestManagedDomainList', + type: ResolverFirewallDomainListType.MANAGED, + kmsKey: new cdk.aws_kms.Key(stack, 'TestManagedDomainListKey', {}), + logRetentionInDays: 3653, +}); + +/** + * DNS firewall domain list construct test + */ +describe('ResolverFirewallDomainList', () => { + /** + * DNS firewall domain list count test + */ + test(`${testNamePrefix} DNS firewall domain list count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Route53Resolver::FirewallDomainList', 1); + }); + + /** + * DNS firewall domain list config test + */ + test(`${testNamePrefix} DNS firewall domain list resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TestDomainList9DC6C806: { + Type: 'AWS::Route53Resolver::FirewallDomainList', + Properties: { + DomainFileUrl: { + 'Fn::Sub': cdk.assertions.Match.stringLikeRegexp( + 's3://cdk-hnb659fds-assets-\\${AWS::AccountId}-\\${AWS::Region}/(\\w)+.zip', + ), + }, + Tags: [ + { + Key: 'Name', + Value: 'TestDomainList', + }, + ], + }, + }, + }, + }); + }); + + /** + * DNS firewall managed domain list count test + */ + test(`${testNamePrefix} DNS firewall managed domain list count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::ResolverManagedDomainList', 1); + }); + + /** + * IAM role count test + */ + test(`${testNamePrefix} IAM role count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 1); + }); + + /** + * Lambda function count test + */ + test(`${testNamePrefix} Lambda function count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 1); + }); + + /** + * DNS firewall managed domain list resource config test + */ + test(`${testNamePrefix} DNS firewall managed domain list resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TestManagedDomainListE1CDFDDE: { + Type: 'Custom::ResolverManagedDomainList', + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomResolverManagedDomainListCustomResourceProviderHandler9F7C9581', 'Arn'], + }, + listName: 'TestManagedDomainList', + region: { + Ref: 'AWS::Region', + }, + }, + }, + }, + }); + }); + + /** + * IAM role resource config test + */ + test(`${testNamePrefix} IAM role resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomResolverManagedDomainListCustomResourceProviderRole33DECC65: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: ['route53resolver:ListFirewallDomainLists'], + Effect: 'Allow', + Resource: '*', + }, + ], + }, + }, + ], + }, + }, + }, + }); + }); + + /** + * Lambda function resource config test + */ + test(`${testNamePrefix} Lambda function resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomResolverManagedDomainListCustomResourceProviderHandler9F7C9581: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: { + 'Fn::Sub': 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', + }, + S3Key: cdk.assertions.Match.stringLikeRegexp('\\w+.zip'), + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomResolverManagedDomainListCustomResourceProviderRole33DECC65', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-route-53-resolver/firewall-rule-group.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-route-53-resolver/firewall-rule-group.test.ts new file mode 100644 index 000000000..8e26679b7 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-route-53-resolver/firewall-rule-group.test.ts @@ -0,0 +1,107 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; + +import { + ResolverFirewallRuleGroup, + ResolverFirewallRuleGroupAssociation, +} from '../../lib/aws-route-53-resolver/firewall-rule-group'; + +const testNamePrefix = 'Construct(ResolverFirewallRuleGroup): '; + +const ruleProps = { + action: 'BLOCK', + firewallDomainListId: 'TestDomainList', + priority: 101, + blockResponse: 'NXDOMAIN', +}; + +const stack = new cdk.Stack(); + +const ruleGroup = new ResolverFirewallRuleGroup(stack, 'TestRuleGroup', { + firewallRules: [ruleProps], + name: 'TestRuleGroup', + tags: [], +}); + +new ResolverFirewallRuleGroupAssociation(stack, 'TestRuleGroupAssoc', { + firewallRuleGroupId: ruleGroup.groupId, + priority: 101, + vpcId: 'TestVpc', +}); + +describe('ResolverFirewallRuleGroup', () => { + /** + * DNS firewall rule group count test + */ + test(`${testNamePrefix} DNS firewall rule group count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Route53Resolver::FirewallRuleGroup', 1); + }); + + /** + * DNS firewall rule group association count test + */ + test(`${testNamePrefix} DNS firewall rule group association count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Route53Resolver::FirewallRuleGroupAssociation', 1); + }); + + /** + * DNS firewall rule group configuration test + */ + test(`${testNamePrefix} DNS firewall rule group resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TestRuleGroup43F9213A: { + Type: 'AWS::Route53Resolver::FirewallRuleGroup', + Properties: { + FirewallRules: [ + { + Action: 'BLOCK', + BlockResponse: 'NXDOMAIN', + FirewallDomainListId: 'TestDomainList', + Priority: 101, + }, + ], + Tags: [ + { + Key: 'Name', + Value: 'TestRuleGroup', + }, + ], + }, + }, + }, + }); + }); + + /** + * DNS firewall rule group association configuration test + */ + test(`${testNamePrefix} DNS firewall rule group association resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TestRuleGroupAssoc48F4678D: { + Type: 'AWS::Route53Resolver::FirewallRuleGroupAssociation', + Properties: { + FirewallRuleGroupId: { + Ref: 'TestRuleGroup43F9213A', + }, + Priority: 101, + VpcId: 'TestVpc', + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-route-53-resolver/query-logging-config.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-route-53-resolver/query-logging-config.test.ts new file mode 100644 index 000000000..8506703a3 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-route-53-resolver/query-logging-config.test.ts @@ -0,0 +1,135 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; + +import { + QueryLoggingConfig, + QueryLoggingConfigAssociation, +} from '../../lib/aws-route-53-resolver/query-logging-config'; + +const testNamePrefix = 'Construct(QueryLoggingConfig): '; + +const stack = new cdk.Stack(); + +// Instantiate resources required for construct +const bucket = cdk.aws_s3.Bucket.fromBucketName(stack, 'TestBucket', 'testbucket'); +const logGroup = new cdk.aws_logs.LogGroup(stack, 'TestLogGroup'); + +// S3 query logging config +const s3Config = new QueryLoggingConfig(stack, 'S3QueryLoggingTest', { + destination: bucket, + name: 'S3QueryLoggingTest', +}); + +// CloudWatch Logs query logging config +new QueryLoggingConfig(stack, 'CwlQueryLoggingTest', { + destination: logGroup, + name: 'CwlQueryLoggingTest', + organizationId: 'o-123test', +}); + +// Config association +new QueryLoggingConfigAssociation(stack, 'TestQueryLoggingAssoc', { + resolverQueryLogConfigId: s3Config.logId, + vpcId: 'TestVpc', +}); + +describe('QueryLoggingConfig', () => { + /** + * Query logging config count test + */ + test(`${testNamePrefix} Query log configuration count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Route53Resolver::ResolverQueryLoggingConfig', 2); + }); + + /** + * Query logging config association count test + */ + test(`${testNamePrefix} Query log configuration association count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs( + 'AWS::Route53Resolver::ResolverQueryLoggingConfigAssociation', + 1, + ); + }); + + /** + * CloudWatch log group resource policy count test + */ + test(`${testNamePrefix} Log group policy count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Logs::ResourcePolicy', 1); + }); + + /** + * S3 Query Logging config resource config test + */ + test(`${testNamePrefix} S3 query logging config resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + S3QueryLoggingTestA05F494B: { + Type: 'AWS::Route53Resolver::ResolverQueryLoggingConfig', + Properties: { + DestinationArn: { + 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':s3:::testbucket']], + }, + }, + }, + }, + }); + }); + + /** + * CloudWatch log query logging config resource config test + */ + test(`${testNamePrefix} CWL query logging config resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CwlQueryLoggingTest70DD9614: { + Type: 'AWS::Route53Resolver::ResolverQueryLoggingConfig', + Properties: { + DestinationArn: { + 'Fn::GetAtt': ['TestLogGroup4EEF7AD4', 'Arn'], + }, + }, + }, + }, + }); + }); + + /** + * CloudWatch Logs resource policy resource config test + */ + test(`${testNamePrefix} CloudWatch Logs resource policy resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TestLogGroupPolicyResourcePolicyFDE53895: { + Type: 'AWS::Logs::ResourcePolicy', + Properties: { + PolicyDocument: { + 'Fn::Join': [ + '', + [ + '{"Statement":[{"Action":["logs:CreateLogStream","logs:PutLogEvents"],"Condition":{"StringEquals":{"aws:PrincipalOrgId":"o-123test"}},"Effect":"Allow","Principal":{"Service":"delivery.logs.amazonaws.com"},"Resource":"', + { + 'Fn::GetAtt': ['TestLogGroup4EEF7AD4', 'Arn'], + }, + ':log-stream:*","Sid":"Allow log delivery access"}],"Version":"2012-10-17"}', + ], + ], + }, + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-route-53-resolver/resolver-endpoint.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-route-53-resolver/resolver-endpoint.test.ts new file mode 100644 index 000000000..9888bf04b --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-route-53-resolver/resolver-endpoint.test.ts @@ -0,0 +1,61 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; + +import { ResolverEndpoint } from '../../lib/aws-route-53-resolver/resolver-endpoint'; + +const testNamePrefix = 'Construct(ResolverEndpoint): '; + +const stack = new cdk.Stack(); + +new ResolverEndpoint(stack, 'TestEndpoint', { + direction: 'OUTBOUND', + ipAddresses: ['subnet-1', 'subnet-2'], + name: 'TestEndpoint', + securityGroupIds: ['sg-123test'], + tags: [], +}); + +describe('ResolverEndpoint', () => { + /** + * Resolver endpoint resource count tets + */ + test(`${testNamePrefix} Resolver endpoint resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Route53Resolver::ResolverEndpoint', 1); + }); + + /** + * Resolver endpoint resource configuration test + */ + test(`${testNamePrefix} Resolver endpoint resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TestEndpoint4E197ABD: { + Type: 'AWS::Route53Resolver::ResolverEndpoint', + Properties: { + Direction: 'OUTBOUND', + IpAddresses: [{ SubnetId: 'subnet-1' }, { SubnetId: 'subnet-2' }], + SecurityGroupIds: ['sg-123test'], + Tags: [ + { + Key: 'Name', + Value: 'TestEndpoint', + }, + ], + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-route-53-resolver/resolver-rule.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-route-53-resolver/resolver-rule.test.ts new file mode 100644 index 000000000..fad239233 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-route-53-resolver/resolver-rule.test.ts @@ -0,0 +1,104 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; + +import { ResolverRule, ResolverRuleAssociation } from '../../lib/aws-route-53-resolver/resolver-rule'; + +const testNamePrefix = 'Construct(ResolverRule): '; + +const stack = new cdk.Stack(); + +const ipAddresses = [{ ip: '1.1.1.1' }, { ip: '2.2.2.2' }]; + +const rule = new ResolverRule(stack, 'TestResolverRule', { + domainName: 'test.com', + name: 'TestResolverRule', + resolverEndpointId: 'TestEndpoint', + targetIps: ipAddresses, + tags: [], + kmsKey: new cdk.aws_kms.Key(stack, 'CustomKey', {}), + logRetentionInDays: 3653, +}); + +new ResolverRuleAssociation(stack, 'TestResolverRuleAssoc', { + resolverRuleId: rule.ruleId, + vpcId: 'TestVpc', +}); + +describe('ResolverRule', () => { + /** + * Resolver rule count test + */ + test(`${testNamePrefix} Resolver rule count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Route53Resolver::ResolverRule', 1); + }); + + /** + * Resolver rule association count test + */ + test(`${testNamePrefix} Resolver rule association count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Route53Resolver::ResolverRuleAssociation', 1); + }); + + /** + * Resolver rule resource configuration test + */ + test(`${testNamePrefix} Resolver rule resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TestResolverRule183FBE0C: { + Type: 'AWS::Route53Resolver::ResolverRule', + Properties: { + DomainName: 'test.com', + ResolverEndpointId: 'TestEndpoint', + RuleType: 'FORWARD', + TargetIps: [ + { + Ip: '1.1.1.1', + }, + { + Ip: '2.2.2.2', + }, + ], + Tags: [ + { + Key: 'Name', + Value: 'TestResolverRule', + }, + ], + }, + }, + }, + }); + }); + + /** + * Resolver rule association resource configuration test + */ + test(`${testNamePrefix} Resolver rule association resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TestResolverRuleAssoc7E0DCDC2: { + Type: 'AWS::Route53Resolver::ResolverRuleAssociation', + Properties: { + ResolverRuleId: { + 'Fn::GetAtt': ['TestResolverRule183FBE0C', 'ResolverRuleId'], + }, + VPCId: 'TestVpc', + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-route-53/__snapshots__/hosted-zone.test.ts.snap b/source/packages/@aws-accelerator/constructs/test/aws-route-53/__snapshots__/hosted-zone.test.ts.snap new file mode 100644 index 000000000..ab8f4ea52 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-route-53/__snapshots__/hosted-zone.test.ts.snap @@ -0,0 +1,22 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`HostedZone Construct(HostedZone): Snapshot Test 1`] = ` +Object { + "Resources": Object { + "TestHostedZone68F306E4": Object { + "Properties": Object { + "Name": "s3-global.accesspoint.aws.com", + "VPCs": Array [ + Object { + "VPCId": "Test", + "VPCRegion": Object { + "Ref": "AWS::Region", + }, + }, + ], + }, + "Type": "AWS::Route53::HostedZone", + }, + }, +} +`; diff --git a/source/packages/@aws-accelerator/constructs/test/aws-route-53/__snapshots__/record-set.test.ts.snap b/source/packages/@aws-accelerator/constructs/test/aws-route-53/__snapshots__/record-set.test.ts.snap new file mode 100644 index 000000000..4ef9cc35b --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-route-53/__snapshots__/record-set.test.ts.snap @@ -0,0 +1,140 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RecordSet Construct(RecordSet): Snapshot Test 1`] = ` +Object { + "Resources": Object { + "TestHostedZone68F306E4": Object { + "Properties": Object { + "Name": "s3-global.accesspoint.aws.com", + "VPCs": Array [ + Object { + "VPCId": "Test", + "VPCRegion": Object { + "Ref": "AWS::Region", + }, + }, + ], + }, + "Type": "AWS::Route53::HostedZone", + }, + "TestRecordSetED81F5C1": Object { + "Properties": Object { + "AliasTarget": Object { + "DNSName": Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + ":", + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::GetAtt": Array [ + "TestVpcEndpointF7CADE71", + "DnsEntries", + ], + }, + ], + }, + ], + }, + ], + }, + "HostedZoneId": Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + ":", + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::GetAtt": Array [ + "TestVpcEndpointF7CADE71", + "DnsEntries", + ], + }, + ], + }, + ], + }, + ], + }, + }, + "HostedZoneId": Object { + "Ref": "TestHostedZone68F306E4", + }, + "Name": "s3-global.accesspoint.aws.com", + "Type": "A", + }, + "Type": "AWS::Route53::RecordSet", + }, + "TestSecurityGroupDA4B5F83": Object { + "Properties": Object { + "GroupDescription": "AWS Private Endpoint Zone", + "GroupName": "TestSecurityGroup", + "Tags": Array [ + Object { + "Key": "Name", + "Value": "TestSecurityGroup", + }, + ], + "VpcId": "Test", + }, + "Type": "AWS::EC2::SecurityGroup", + }, + "TestVpcEndpointF7CADE71": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "*", + "Condition": Object { + "StringEquals": Object { + "aws:PrincipalOrgID": Array [ + "organizationId", + ], + }, + }, + "Effect": "Allow", + "Principal": Object { + "AWS": "*", + }, + "Resource": "*", + "Sid": "AccessToTrustedPrincipalsAndResources", + }, + ], + "Version": "2012-10-17", + }, + "PrivateDnsEnabled": false, + "SecurityGroupIds": Array [ + Object { + "Ref": "TestSecurityGroupDA4B5F83", + }, + ], + "ServiceName": Object { + "Fn::Join": Array [ + "", + Array [ + "com.amazonaws.", + Object { + "Ref": "AWS::Region", + }, + ".ec2", + ], + ], + }, + "SubnetIds": Array [ + "Test1", + "Test2", + ], + "VpcEndpointType": "Interface", + "VpcId": "Test", + }, + "Type": "AWS::EC2::VPCEndpoint", + }, + }, +} +`; diff --git a/source/packages/@aws-accelerator/constructs/test/aws-route-53/hosted-zone.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-route-53/hosted-zone.test.ts new file mode 100644 index 000000000..9469431ad --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-route-53/hosted-zone.test.ts @@ -0,0 +1,72 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; + +import { SynthUtils } from '@aws-cdk/assert'; + +import { HostedZone } from '../../index'; + +const testNamePrefix = 'Construct(HostedZone): '; + +//Initialize stack for snapshot test and resource configuration test +const stack = new cdk.Stack(); +const hostedZoneName = HostedZone.getHostedZoneNameForService('s3-global.accesspoint', stack.region); + +new HostedZone(stack, `TestHostedZone`, { + hostedZoneName, + vpcId: 'Test', +}); + +/** + * HostedZone construct test + */ +describe('HostedZone', () => { + /** + * Snapshot test + */ + test(`${testNamePrefix} Snapshot Test`, () => { + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); + }); + + /** + * Number of hosted zone test + */ + test(`${testNamePrefix} Hosted zone count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Route53::HostedZone', 1); + }); + + /** + * HostedZone resource configuration test + */ + test(`${testNamePrefix} HostedZone resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TestHostedZone68F306E4: { + Type: 'AWS::Route53::HostedZone', + Properties: { + Name: 's3-global.accesspoint.aws.com', + VPCs: [ + { + VPCId: 'Test', + VPCRegion: { + Ref: 'AWS::Region', + }, + }, + ], + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-route-53/record-set.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-route-53/record-set.test.ts new file mode 100644 index 000000000..5de6e1cb9 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-route-53/record-set.test.ts @@ -0,0 +1,271 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; + +import { SynthUtils } from '@aws-cdk/assert'; + +import { HostedZone, RecordSet, SecurityGroup, VpcEndpoint } from '../../index'; + +const testNamePrefix = 'Construct(RecordSet): '; + +//Initialize stack for snapshot test and resource configuration test +const stack = new cdk.Stack(); +const hostedZoneName = HostedZone.getHostedZoneNameForService('s3-global.accesspoint', stack.region); + +const hostedZone = new HostedZone(stack, `TestHostedZone`, { + hostedZoneName, + vpcId: 'Test', +}); + +const securityGroup = new SecurityGroup(stack, 'TestSecurityGroup`', { + securityGroupName: 'TestSecurityGroup', + description: `AWS Private Endpoint Zone`, + vpcId: 'Test', + tags: [], +}); + +// Create the interface endpoint +const endpoint = new VpcEndpoint(stack, `TestVpcEndpoint`, { + vpcId: 'Test', + vpcEndpointType: cdk.aws_ec2.VpcEndpointType.INTERFACE, + service: 'ec2', + subnets: ['Test1', 'Test2'], + securityGroups: [securityGroup], + privateDnsEnabled: false, + policyDocument: new cdk.aws_iam.PolicyDocument({ + statements: [ + new cdk.aws_iam.PolicyStatement({ + sid: 'AccessToTrustedPrincipalsAndResources', + actions: ['*'], + effect: cdk.aws_iam.Effect.ALLOW, + resources: ['*'], + principals: [new cdk.aws_iam.AnyPrincipal()], + conditions: { + StringEquals: { + 'aws:PrincipalOrgID': ['organizationId'], + }, + }, + }), + ], + }), +}); + +new RecordSet(stack, `TestRecordSet`, { + type: 'A', + name: hostedZoneName, + hostedZone: hostedZone, + dnsName: endpoint.dnsName, + hostedZoneId: endpoint.hostedZoneId, +}); + +/** + * RecordSet construct test + */ +describe('RecordSet', () => { + /** + * Snapshot test + */ + test(`${testNamePrefix} Snapshot Test`, () => { + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); + }); + + /** + * Number of HostedZone test + */ + test(`${testNamePrefix} Hosted zone resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Route53::HostedZone', 1); + }); + + /** + * Number of RecordSet test + */ + test(`${testNamePrefix} RecordSet resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Route53::RecordSet', 1); + }); + + /** + * Number of SecurityGroup test + */ + test(`${testNamePrefix} SecurityGroup resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::EC2::SecurityGroup', 1); + }); + + /** + * Number of VPCEndpoint test + */ + test(`${testNamePrefix} VPCEndpoint resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::EC2::VPCEndpoint', 1); + }); + + /** + * HostedZone resource configuration test + */ + test(`${testNamePrefix} HostedZone resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TestHostedZone68F306E4: { + Type: 'AWS::Route53::HostedZone', + Properties: { + Name: 's3-global.accesspoint.aws.com', + VPCs: [ + { + VPCId: 'Test', + VPCRegion: { + Ref: 'AWS::Region', + }, + }, + ], + }, + }, + }, + }); + }); + + /** + * RecordSet resource configuration test + */ + test(`${testNamePrefix} RecordSet resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TestRecordSetED81F5C1: { + Type: 'AWS::Route53::RecordSet', + Properties: { + AliasTarget: { + DNSName: { + 'Fn::Select': [ + 1, + { + 'Fn::Split': [ + ':', + { + 'Fn::Select': [ + 0, + { + 'Fn::GetAtt': ['TestVpcEndpointF7CADE71', 'DnsEntries'], + }, + ], + }, + ], + }, + ], + }, + HostedZoneId: { + 'Fn::Select': [ + 0, + { + 'Fn::Split': [ + ':', + { + 'Fn::Select': [ + 0, + { + 'Fn::GetAtt': ['TestVpcEndpointF7CADE71', 'DnsEntries'], + }, + ], + }, + ], + }, + ], + }, + }, + HostedZoneId: { + Ref: 'TestHostedZone68F306E4', + }, + Name: 's3-global.accesspoint.aws.com', + Type: 'A', + }, + }, + }, + }); + }); + + /** + * SecurityGroup resource configuration test + */ + test(`${testNamePrefix} SecurityGroup resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TestSecurityGroupDA4B5F83: { + Type: 'AWS::EC2::SecurityGroup', + Properties: { + GroupDescription: 'AWS Private Endpoint Zone', + GroupName: 'TestSecurityGroup', + Tags: [ + { + Key: 'Name', + Value: 'TestSecurityGroup', + }, + ], + VpcId: 'Test', + }, + }, + }, + }); + }); + + /** + * VPCEndpoint resource configuration test + */ + test(`${testNamePrefix} VPCEndpoint resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + TestVpcEndpointF7CADE71: { + Type: 'AWS::EC2::VPCEndpoint', + Properties: { + PolicyDocument: { + Statement: [ + { + Action: '*', + Condition: { + StringEquals: { + 'aws:PrincipalOrgID': ['organizationId'], + }, + }, + Effect: 'Allow', + Principal: { + AWS: '*', + }, + Resource: '*', + Sid: 'AccessToTrustedPrincipalsAndResources', + }, + ], + Version: '2012-10-17', + }, + PrivateDnsEnabled: false, + SecurityGroupIds: [ + { + Ref: 'TestSecurityGroupDA4B5F83', + }, + ], + ServiceName: { + 'Fn::Join': [ + '', + [ + 'com.amazonaws.', + { + Ref: 'AWS::Region', + }, + '.ec2', + ], + ], + }, + SubnetIds: ['Test1', 'Test2'], + VpcEndpointType: 'Interface', + VpcId: 'Test', + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-s3/__snapshots__/central-logs-buckets.test.ts.snap b/source/packages/@aws-accelerator/constructs/test/aws-s3/__snapshots__/central-logs-buckets.test.ts.snap new file mode 100644 index 000000000..e0c00ec92 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-s3/__snapshots__/central-logs-buckets.test.ts.snap @@ -0,0 +1,648 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CentralLogsBucket Construct(CentralLogsBucket): Snapshot Test 1`] = ` +Object { + "Resources": Object { + "AccessLogsBucketCmkAliasD1876683": Object { + "Properties": Object { + "AliasName": "alias/accelerator/s3-access-logs/s3", + "TargetKeyId": Object { + "Fn::GetAtt": Array [ + "AccessLogsBucketCmkECACF392", + "Arn", + ], + }, + }, + "Type": "AWS::KMS::Alias", + }, + "AccessLogsBucketCmkECACF392": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "Description": "AWS Accelerator S3 Access Logs Bucket CMK", + "EnableKeyRotation": true, + "KeyPolicy": Object { + "Statement": Array [ + Object { + "Action": "kms:*", + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::", + Object { + "Ref": "AWS::AccountId", + }, + ":root", + ], + ], + }, + }, + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::KMS::Key", + "UpdateReplacePolicy": "Retain", + }, + "AccessLogsBucketFA218D2A": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "KMSMasterKeyID": Object { + "Fn::GetAtt": Array [ + "AccessLogsBucketCmkECACF392", + "Arn", + ], + }, + "SSEAlgorithm": "aws:kms", + }, + }, + ], + }, + "BucketName": Object { + "Fn::Join": Array [ + "", + Array [ + "aws-accelerator-s3-access-logs-", + Object { + "Ref": "AWS::AccountId", + }, + "-", + Object { + "Ref": "AWS::Region", + }, + ], + ], + }, + "LifecycleConfiguration": Object { + "Rules": Array [ + Object { + "AbortIncompleteMultipartUpload": Object { + "DaysAfterInitiation": 1, + }, + "ExpirationInDays": 1825, + "ExpiredObjectDeleteMarker": false, + "Id": Object { + "Fn::Join": Array [ + "", + Array [ + "LifecycleRuleaws-accelerator-s3-access-logs-", + Object { + "Ref": "AWS::AccountId", + }, + "-", + Object { + "Ref": "AWS::Region", + }, + ], + ], + }, + "NoncurrentVersionExpirationInDays": 1825, + "NoncurrentVersionTransitions": Array [ + Object { + "StorageClass": "DEEP_ARCHIVE", + "TransitionInDays": 366, + }, + ], + "Status": "Enabled", + "Transitions": Array [ + Object { + "StorageClass": "DEEP_ARCHIVE", + "TransitionInDays": 365, + }, + ], + }, + ], + }, + "OwnershipControls": Object { + "Rules": Array [ + Object { + "ObjectOwnership": "BucketOwnerPreferred", + }, + ], + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "AccessLogsBucketPolicy00F12803": Object { + "Properties": Object { + "Bucket": Object { + "Ref": "AccessLogsBucketFA218D2A", + }, + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "s3:*", + "Condition": Object { + "Bool": Object { + "aws:SecureTransport": "false", + }, + }, + "Effect": "Deny", + "Principal": Object { + "AWS": "*", + }, + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "AccessLogsBucketFA218D2A", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "AccessLogsBucketFA218D2A", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + "Sid": "deny-insecure-connections", + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::S3::BucketPolicy", + }, + "CentralLogsBucket447B5C59": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "KMSMasterKeyID": Object { + "Fn::GetAtt": Array [ + "CentralLogsBucketCmkBA0AB2FC", + "Arn", + ], + }, + "SSEAlgorithm": "aws:kms", + }, + }, + ], + }, + "BucketName": Object { + "Fn::Join": Array [ + "", + Array [ + "aws-accelerator-central-logs-", + Object { + "Ref": "AWS::AccountId", + }, + "-", + Object { + "Ref": "AWS::Region", + }, + ], + ], + }, + "LifecycleConfiguration": Object { + "Rules": Array [ + Object { + "AbortIncompleteMultipartUpload": Object { + "DaysAfterInitiation": 1, + }, + "ExpirationInDays": 1825, + "ExpiredObjectDeleteMarker": false, + "Id": Object { + "Fn::Join": Array [ + "", + Array [ + "LifecycleRuleaws-accelerator-central-logs-", + Object { + "Ref": "AWS::AccountId", + }, + "-", + Object { + "Ref": "AWS::Region", + }, + ], + ], + }, + "NoncurrentVersionExpirationInDays": 1825, + "NoncurrentVersionTransitions": Array [ + Object { + "StorageClass": "DEEP_ARCHIVE", + "TransitionInDays": 366, + }, + ], + "Status": "Enabled", + "Transitions": Array [ + Object { + "StorageClass": "DEEP_ARCHIVE", + "TransitionInDays": 365, + }, + ], + }, + ], + }, + "LoggingConfiguration": Object { + "DestinationBucketName": Object { + "Ref": "AccessLogsBucketFA218D2A", + }, + "LogFilePrefix": Object { + "Fn::Join": Array [ + "", + Array [ + "aws-accelerator-central-logs-", + Object { + "Ref": "AWS::AccountId", + }, + "-", + Object { + "Ref": "AWS::Region", + }, + "/", + ], + ], + }, + }, + "OwnershipControls": Object { + "Rules": Array [ + Object { + "ObjectOwnership": "BucketOwnerPreferred", + }, + ], + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "CentralLogsBucketCmkAlias286EB783": Object { + "Properties": Object { + "AliasName": "alias/accelerator/central-logs/s3", + "TargetKeyId": Object { + "Fn::GetAtt": Array [ + "CentralLogsBucketCmkBA0AB2FC", + "Arn", + ], + }, + }, + "Type": "AWS::KMS::Alias", + }, + "CentralLogsBucketCmkBA0AB2FC": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "Description": "AWS Accelerator Central Logs Bucket CMK", + "EnableKeyRotation": true, + "KeyPolicy": Object { + "Statement": Array [ + Object { + "Action": "kms:*", + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::", + Object { + "Ref": "AWS::AccountId", + }, + ":root", + ], + ], + }, + }, + "Resource": "*", + }, + Object { + "Action": "kms:*", + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::", + Object { + "Ref": "AWS::AccountId", + }, + ":root", + ], + ], + }, + }, + "Resource": "*", + "Sid": "Enable IAM User Permissions", + }, + Object { + "Action": Array [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:GenerateDataKey", + "kms:GenerateDataKeyWithoutPlaintext", + "kms:GenerateRandom", + "kms:GetKeyPolicy", + "kms:GetKeyRotationStatus", + "kms:ListAliases", + "kms:ListGrants", + "kms:ListKeyPolicies", + "kms:ListKeys", + "kms:ListResourceTags", + "kms:ListRetirableGrants", + "kms:ReEncryptFrom", + "kms:ReEncryptTo", + ], + "Effect": "Allow", + "Principal": Object { + "Service": "s3.amazonaws.com", + }, + "Resource": "*", + "Sid": "Allow S3 use of the key", + }, + Object { + "Action": Array [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:GenerateDataKey", + "kms:GenerateDataKeyPair", + "kms:GenerateDataKeyPairWithoutPlaintext", + "kms:GenerateDataKeyWithoutPlaintext", + "kms:ReEncryptFrom", + "kms:ReEncryptTo", + ], + "Effect": "Allow", + "Principal": Object { + "Service": Array [ + "config.amazonaws.com", + "cloudtrail.amazonaws.com", + "delivery.logs.amazonaws.com", + ], + }, + "Resource": "*", + "Sid": "Allow AWS Services to encrypt and describe logs", + }, + Object { + "Action": Array [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:GenerateDataKey", + "kms:GenerateDataKeyPair", + "kms:GenerateDataKeyPairWithoutPlaintext", + "kms:GenerateDataKeyWithoutPlaintext", + "kms:ReEncryptFrom", + "kms:ReEncryptTo", + ], + "Condition": Object { + "StringEquals": Object { + "aws:PrincipalOrgID": "acceleratorOrg", + }, + }, + "Effect": "Allow", + "Principal": Object { + "AWS": "*", + }, + "Resource": "*", + "Sid": "Allow Organization use of the key", + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::KMS::Key", + "UpdateReplacePolicy": "Retain", + }, + "CentralLogsBucketCrossAccountCentralBucketKMSArnSsmParamAccessRole83E55C59": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Condition": Object { + "StringEquals": Object { + "aws:PrincipalOrgID": "acceleratorOrg", + }, + }, + "Effect": "Allow", + "Principal": Object { + "AWS": "*", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "ssm:GetParameters", + "ssm:GetParameter", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":ssm:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":parameter", + Object { + "Ref": "CentralLogsBucketSsmParamCentralAccountBucketKMSArn57B61EA3", + }, + ], + ], + }, + }, + Object { + "Action": "ssm:DescribeParameters", + "Effect": "Allow", + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "default", + }, + ], + "RoleName": Object { + "Fn::Join": Array [ + "", + Array [ + "AWSAccelerator-CentralBucketKMSArnSsmParam-", + Object { + "Ref": "AWS::Region", + }, + ], + ], + }, + }, + "Type": "AWS::IAM::Role", + }, + "CentralLogsBucketPolicyC0B90E7C": Object { + "Properties": Object { + "Bucket": Object { + "Ref": "CentralLogsBucket447B5C59", + }, + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "s3:*", + "Condition": Object { + "Bool": Object { + "aws:SecureTransport": "false", + }, + }, + "Effect": "Deny", + "Principal": Object { + "AWS": "*", + }, + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "CentralLogsBucket447B5C59", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "CentralLogsBucket447B5C59", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + "Sid": "deny-insecure-connections", + }, + Object { + "Action": "s3:PutObject", + "Condition": Object { + "StringEquals": Object { + "s3:x-amz-acl": "bucket-owner-full-control", + }, + }, + "Effect": "Allow", + "Principal": Object { + "Service": Array [ + "cloudtrail.amazonaws.com", + "config.amazonaws.com", + "delivery.logs.amazonaws.com", + ], + }, + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "CentralLogsBucket447B5C59", + "Arn", + ], + }, + "/*", + ], + ], + }, + }, + Object { + "Action": Array [ + "s3:GetBucketAcl", + "s3:ListBucket", + ], + "Effect": "Allow", + "Principal": Object { + "Service": Array [ + "cloudtrail.amazonaws.com", + "config.amazonaws.com", + "delivery.logs.amazonaws.com", + ], + }, + "Resource": Object { + "Fn::GetAtt": Array [ + "CentralLogsBucket447B5C59", + "Arn", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::S3::BucketPolicy", + }, + "CentralLogsBucketSsmParamCentralAccountBucketKMSArn57B61EA3": Object { + "Properties": Object { + "Name": "/accelerator/logging/central-bucket/kms/arn", + "Type": "String", + "Value": Object { + "Fn::GetAtt": Array [ + "CentralLogsBucketCmkBA0AB2FC", + "Arn", + ], + }, + }, + "Type": "AWS::SSM::Parameter", + }, + }, +} +`; diff --git a/source/packages/@aws-accelerator/constructs/test/aws-s3/central-logs-buckets.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-s3/central-logs-buckets.test.ts new file mode 100644 index 000000000..3c56a2655 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-s3/central-logs-buckets.test.ts @@ -0,0 +1,535 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { SynthUtils } from '@aws-cdk/assert'; +import { Bucket, BucketEncryptionType, CentralLogsBucket } from '@aws-accelerator/constructs'; + +const testNamePrefix = 'Construct(CentralLogsBucket): '; + +//Initialize stack for snapshot test and resource configuration test +const stack = new cdk.Stack(); + +new CentralLogsBucket(stack, 'CentralLogsBucket', { + s3BucketName: `aws-accelerator-central-logs-${stack.account}-${stack.region}`, + serverAccessLogsBucket: new Bucket(stack, 'AccessLogsBucket', { + encryptionType: BucketEncryptionType.SSE_KMS, + s3BucketName: `aws-accelerator-s3-access-logs-${stack.account}-${stack.region}`, + kmsAliasName: 'alias/accelerator/s3-access-logs/s3', + kmsDescription: 'AWS Accelerator S3 Access Logs Bucket CMK', + }), + kmsAliasName: 'alias/accelerator/central-logs/s3', + kmsDescription: 'AWS Accelerator Central Logs Bucket CMK', + organizationId: 'acceleratorOrg', +}); + +/** + * CentralLogsBucket construct test + */ +describe('CentralLogsBucket', () => { + /** + * Snapshot test + */ + test(`${testNamePrefix} Snapshot Test`, () => { + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); + }); + + /** + * Number of bucket test + */ + test(`${testNamePrefix} S3 bucket count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::S3::Bucket', 2); + }); + + /** + * Number of bucket KMS test + */ + test(`${testNamePrefix} bucket key count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::KMS::Key', 2); + }); + + /** + * Number of bucket KMS alias test + */ + test(`${testNamePrefix} Bucket KMS alias count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::KMS::Alias', 2); + }); + + /** + * Number of bucket IAM Roles + */ + test(`${testNamePrefix} IAM Role count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 1); + }); + + /** + * Number of SSM Parameters + */ + test(`${testNamePrefix} SSM Parameter count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::SSM::Parameter', 1); + }); + + /** + * AccessLogsBucket configuration configuration test + */ + test(`${testNamePrefix} AccessLogsBucket configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + AccessLogsBucketFA218D2A: { + Type: 'AWS::S3::Bucket', + DeletionPolicy: 'Retain', + UpdateReplacePolicy: 'Retain', + Properties: { + AccessControl: 'LogDeliveryWrite', + BucketEncryption: { + ServerSideEncryptionConfiguration: [ + { + ServerSideEncryptionByDefault: { + KMSMasterKeyID: { + 'Fn::GetAtt': ['AccessLogsBucketCmkECACF392', 'Arn'], + }, + SSEAlgorithm: 'aws:kms', + }, + }, + ], + }, + BucketName: { + 'Fn::Join': [ + '', + [ + 'aws-accelerator-s3-access-logs-', + { + Ref: 'AWS::AccountId', + }, + '-', + { + Ref: 'AWS::Region', + }, + ], + ], + }, + OwnershipControls: { + Rules: [ + { + ObjectOwnership: 'BucketOwnerPreferred', + }, + ], + }, + PublicAccessBlockConfiguration: { + BlockPublicAcls: true, + BlockPublicPolicy: true, + IgnorePublicAcls: true, + RestrictPublicBuckets: true, + }, + VersioningConfiguration: { + Status: 'Enabled', + }, + }, + }, + }, + }); + }); + + /** + * AccessLogsBucket KMS configuration test + */ + test(`${testNamePrefix} AccessLogsBucket KMS configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + AccessLogsBucketCmkECACF392: { + Type: 'AWS::KMS::Key', + UpdateReplacePolicy: 'Retain', + DeletionPolicy: 'Retain', + Properties: { + EnableKeyRotation: true, + KeyPolicy: { + Version: '2012-10-17', + Statement: [ + { + Action: 'kms:*', + Effect: 'Allow', + Principal: { + AWS: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::', + { + Ref: 'AWS::AccountId', + }, + ':root', + ], + ], + }, + }, + Resource: '*', + }, + ], + }, + }, + }, + }, + }); + }); + + /** + * AccessLogsBucketCmkAlias KMS Alias configuration test + */ + test(`${testNamePrefix} AccessLogsBucket KMS alias configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + AccessLogsBucketCmkAliasD1876683: { + Type: 'AWS::KMS::Alias', + Properties: { + AliasName: 'alias/accelerator/s3-access-logs/s3', + TargetKeyId: { + 'Fn::GetAtt': ['AccessLogsBucketCmkECACF392', 'Arn'], + }, + }, + }, + }, + }); + }); + + /** + * CentralLogsBucket configuration test + */ + test(`${testNamePrefix} CentralLogsBucket configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CentralLogsBucket447B5C59: { + Type: 'AWS::S3::Bucket', + UpdateReplacePolicy: 'Retain', + DeletionPolicy: 'Retain', + Properties: { + LoggingConfiguration: { + DestinationBucketName: { + Ref: 'AccessLogsBucketFA218D2A', + }, + }, + BucketEncryption: { + ServerSideEncryptionConfiguration: [ + { + ServerSideEncryptionByDefault: { + KMSMasterKeyID: { + 'Fn::GetAtt': ['CentralLogsBucketCmkBA0AB2FC', 'Arn'], + }, + SSEAlgorithm: 'aws:kms', + }, + }, + ], + }, + BucketName: { + 'Fn::Join': [ + '', + [ + 'aws-accelerator-central-logs-', + { + Ref: 'AWS::AccountId', + }, + '-', + { + Ref: 'AWS::Region', + }, + ], + ], + }, + OwnershipControls: { + Rules: [ + { + ObjectOwnership: 'BucketOwnerPreferred', + }, + ], + }, + PublicAccessBlockConfiguration: { + BlockPublicAcls: true, + BlockPublicPolicy: true, + IgnorePublicAcls: true, + RestrictPublicBuckets: true, + }, + VersioningConfiguration: { + Status: 'Enabled', + }, + }, + }, + }, + }); + }); + + /** + * CentralLogsBucket KMS configuration test + */ + test(`${testNamePrefix} CentralLogsBucket KMS configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CentralLogsBucketCmkBA0AB2FC: { + Type: 'AWS::KMS::Key', + UpdateReplacePolicy: 'Retain', + DeletionPolicy: 'Retain', + Properties: { + EnableKeyRotation: true, + KeyPolicy: { + Version: '2012-10-17', + Statement: [ + { + Action: 'kms:*', + Effect: 'Allow', + Principal: { + AWS: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::', + { + Ref: 'AWS::AccountId', + }, + ':root', + ], + ], + }, + }, + Resource: '*', + }, + { + Action: 'kms:*', + Effect: 'Allow', + Principal: { + AWS: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::', + { + Ref: 'AWS::AccountId', + }, + ':root', + ], + ], + }, + }, + Resource: '*', + Sid: 'Enable IAM User Permissions', + }, + { + Action: [ + 'kms:Decrypt', + 'kms:DescribeKey', + 'kms:Encrypt', + 'kms:GenerateDataKey', + 'kms:GenerateDataKeyWithoutPlaintext', + 'kms:GenerateRandom', + 'kms:GetKeyPolicy', + 'kms:GetKeyRotationStatus', + 'kms:ListAliases', + 'kms:ListGrants', + 'kms:ListKeyPolicies', + 'kms:ListKeys', + 'kms:ListResourceTags', + 'kms:ListRetirableGrants', + 'kms:ReEncryptFrom', + 'kms:ReEncryptTo', + ], + Effect: 'Allow', + Principal: { + Service: 's3.amazonaws.com', + }, + Resource: '*', + Sid: 'Allow S3 use of the key', + }, + { + Action: [ + 'kms:Decrypt', + 'kms:DescribeKey', + 'kms:Encrypt', + 'kms:GenerateDataKey', + 'kms:GenerateDataKeyPair', + 'kms:GenerateDataKeyPairWithoutPlaintext', + 'kms:GenerateDataKeyWithoutPlaintext', + 'kms:ReEncryptFrom', + 'kms:ReEncryptTo', + ], + Effect: 'Allow', + Principal: { + Service: ['config.amazonaws.com', 'cloudtrail.amazonaws.com', 'delivery.logs.amazonaws.com'], + }, + Resource: '*', + Sid: 'Allow AWS Services to encrypt and describe logs', + }, + { + Action: [ + 'kms:Decrypt', + 'kms:DescribeKey', + 'kms:Encrypt', + 'kms:GenerateDataKey', + 'kms:GenerateDataKeyPair', + 'kms:GenerateDataKeyPairWithoutPlaintext', + 'kms:GenerateDataKeyWithoutPlaintext', + 'kms:ReEncryptFrom', + 'kms:ReEncryptTo', + ], + Condition: { + StringEquals: { + 'aws:PrincipalOrgID': 'acceleratorOrg', + }, + }, + Effect: 'Allow', + Principal: { + AWS: '*', + }, + Resource: '*', + Sid: 'Allow Organization use of the key', + }, + ], + }, + }, + }, + }, + }); + }); + + /** + * AccessLogsBucketCmkAlias KMS alias configuration test + */ + test(`${testNamePrefix} CentralLogsBucket KMS alias configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CentralLogsBucketCmkAlias286EB783: { + Type: 'AWS::KMS::Alias', + Properties: { + AliasName: 'alias/accelerator/central-logs/s3', + TargetKeyId: { + 'Fn::GetAtt': ['CentralLogsBucketCmkBA0AB2FC', 'Arn'], + }, + }, + }, + }, + }); + }); + + /** + * SSM Parameter configuration test + */ + test(`${testNamePrefix} SSM Parameter configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CentralLogsBucketSsmParamCentralAccountBucketKMSArn57B61EA3: { + Properties: { + Name: '/accelerator/logging/central-bucket/kms/arn', + Type: 'String', + Value: { + 'Fn::GetAtt': ['CentralLogsBucketCmkBA0AB2FC', 'Arn'], + }, + }, + Type: 'AWS::SSM::Parameter', + }, + }, + }); + }); + + /** + * SSMParameter IAM Role configuration test + */ + test(`${testNamePrefix} SSMParameter IAM Role configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CentralLogsBucketCrossAccountCentralBucketKMSArnSsmParamAccessRole83E55C59: { + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Condition: { + StringEquals: { + 'aws:PrincipalOrgID': 'acceleratorOrg', + }, + }, + Effect: 'Allow', + Principal: { + AWS: '*', + }, + }, + ], + Version: '2012-10-17', + }, + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: ['ssm:GetParameters', 'ssm:GetParameter'], + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':ssm:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':parameter', + { + Ref: 'CentralLogsBucketSsmParamCentralAccountBucketKMSArn57B61EA3', + }, + ], + ], + }, + }, + { + Action: 'ssm:DescribeParameters', + Effect: 'Allow', + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'default', + }, + ], + RoleName: { + 'Fn::Join': [ + '', + [ + 'AWSAccelerator-CentralBucketKMSArnSsmParam-', + { + Ref: 'AWS::Region', + }, + ], + ], + }, + }, + Type: 'AWS::IAM::Role', + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-s3/public-access-block.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-s3/public-access-block.test.ts new file mode 100644 index 000000000..6d912518b --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-s3/public-access-block.test.ts @@ -0,0 +1,158 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { S3PublicAccessBlock } from '@aws-accelerator/constructs'; + +const testNamePrefix = 'Construct(S3PublicAccessBlock): '; + +//Initialize stack for snapshot test and resource configuration test +const stack = new cdk.Stack(); + +new S3PublicAccessBlock(stack, 'S3PublicAccessBlock', { + blockPublicAcls: true, + blockPublicPolicy: true, + ignorePublicAcls: true, + restrictPublicBuckets: true, + accountId: stack.account, + kmsKey: new cdk.aws_kms.Key(stack, 'CustomKey', {}), + logRetentionInDays: 3653, +}); + +/** + * CentralLogsBucket construct test + */ +describe('S3PublicAccessBlock', () => { + /** + * Number of IAM role test + */ + test(`${testNamePrefix} IAM role count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 1); + }); + + /** + * Number of Lambda function test + */ + test(`${testNamePrefix} Lambda function count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 1); + }); + + /** + * Number of CustomResource test + */ + test(`${testNamePrefix} CustomResource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::PutPublicAccessBlock', 1); + }); + + /** + * S3PublicAccessBlock custom resource lambda function configuration test + */ + test(`${testNamePrefix} S3PublicAccessBlock custom resource lambda function configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomS3PutPublicAccessBlockCustomResourceProviderHandler978E227B: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomS3PutPublicAccessBlockCustomResourceProviderRole656EB36E'], + Properties: { + Code: { + S3Bucket: { + 'Fn::Sub': 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', + }, + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomS3PutPublicAccessBlockCustomResourceProviderRole656EB36E', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * S3PublicAccessBlock custom resource iam role test + */ + test(`${testNamePrefix} S3PublicAccessBlock custom resource iam role test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomS3PutPublicAccessBlockCustomResourceProviderRole656EB36E: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: ['s3:PutAccountPublicAccessBlock'], + Effect: 'Allow', + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * S3PublicAccessBlock custom resource test + */ + test(`${testNamePrefix} S3PublicAccessBlock custom resource test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + S3PublicAccessBlock344F906B: { + Type: 'Custom::PutPublicAccessBlock', + DeletionPolicy: 'Delete', + UpdateReplacePolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomS3PutPublicAccessBlockCustomResourceProviderHandler978E227B', 'Arn'], + }, + accountId: { + Ref: 'AWS::AccountId', + }, + blockPublicAcls: true, + blockPublicPolicy: true, + ignorePublicAcls: true, + restrictPublicBuckets: true, + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-securityhub/securityhub-members.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-securityhub/securityhub-members.test.ts new file mode 100644 index 000000000..6038bfd8d --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-securityhub/securityhub-members.test.ts @@ -0,0 +1,168 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { SecurityHubMembers } from '../../index'; + +const testNamePrefix = 'Construct(SecurityHubMembers): '; + +//Initialize stack for snapshot test and resource configuration test +const stack = new cdk.Stack(); + +new SecurityHubMembers(stack, 'SecurityHubMembers', { + kmsKey: new cdk.aws_kms.Key(stack, 'CustomKey', {}), + logRetentionInDays: 3653, +}); + +/** + * SecurityHubMembers construct test + */ +describe('SecurityHubMembers', () => { + /** + * Number of IAM role test + */ + test(`${testNamePrefix} IAM role count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 1); + }); + + /** + * Number of Lambda function test + */ + test(`${testNamePrefix} Lambda function count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 1); + }); + + /** + * Number of CustomResource test + */ + test(`${testNamePrefix} CustomResource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::SecurityHubCreateMembers', 1); + }); + + /** + * SecurityHubMembers custom resource lambda function configuration test + */ + test(`${testNamePrefix} SecurityHubMembers custom resource lambda function configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomSecurityHubCreateMembersCustomResourceProviderHandler31D82BF3: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomSecurityHubCreateMembersCustomResourceProviderRoleFD355CB6'], + Properties: { + Code: { + S3Bucket: { + 'Fn::Sub': 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', + }, + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomSecurityHubCreateMembersCustomResourceProviderRoleFD355CB6', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * SecurityHubMembers custom resource iam role test + */ + test(`${testNamePrefix} SecurityHubMembers custom resource iam role test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomSecurityHubCreateMembersCustomResourceProviderRoleFD355CB6: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: ['organizations:ListAccounts'], + Condition: { + StringLikeIfExists: { + 'organizations:ListAccounts': ['securityhub.amazonaws.com'], + }, + }, + Effect: 'Allow', + Resource: '*', + Sid: 'SecurityHubCreateMembersTaskOrganizationAction', + }, + { + Action: [ + 'securityhub:CreateMembers', + 'securityhub:DeleteMembers', + 'securityhub:DisassociateMembers', + 'securityhub:EnableSecurityHub', + 'securityhub:ListMembers', + 'securityhub:UpdateOrganizationConfiguration', + ], + Effect: 'Allow', + Resource: '*', + Sid: 'SecurityHubCreateMembersTaskSecurityHubActions', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * SecurityHubMembers custom resource test + */ + test(`${testNamePrefix} SecurityHubMembers custom resource test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + SecurityHubMembers2A2B77C4: { + Type: 'Custom::SecurityHubCreateMembers', + UpdateReplacePolicy: 'Delete', + DeletionPolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomSecurityHubCreateMembersCustomResourceProviderHandler31D82BF3', 'Arn'], + }, + region: { + Ref: 'AWS::Region', + }, + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-securityhub/securityhub-organization-admin-account.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-securityhub/securityhub-organization-admin-account.test.ts new file mode 100644 index 000000000..bcd12c3e1 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-securityhub/securityhub-organization-admin-account.test.ts @@ -0,0 +1,222 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { SecurityHubOrganizationAdminAccount } from '../../index'; + +const testNamePrefix = 'Construct(SecurityHubOrganizationAdminAccount): '; + +//Initialize stack for snapshot test and resource configuration test +const stack = new cdk.Stack(); + +new SecurityHubOrganizationAdminAccount(stack, 'SecurityHubOrganizationAdminAccount', { + adminAccountId: stack.account, + kmsKey: new cdk.aws_kms.Key(stack, 'CustomKey', {}), + logRetentionInDays: 3653, +}); + +/** + * SecurityHubOrganizationAdminAccount construct test + */ +describe('SecurityHubOrganizationAdminAccount', () => { + /** + * Number of IAM role test + */ + test(`${testNamePrefix} IAM role count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 1); + }); + + /** + * Number of Lambda function test + */ + test(`${testNamePrefix} Lambda function count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 1); + }); + + /** + * Number of CustomResource test + */ + test(`${testNamePrefix} CustomResource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::SecurityHubEnableOrganizationAdminAccount', 1); + }); + + /** + * EnableOrganizationAdminAccount custom resource lambda function configuration test + */ + test(`${testNamePrefix} EnableOrganizationAdminAccount custom resource lambda function configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomSecurityHubEnableOrganizationAdminAccountCustomResourceProviderHandler194C30B9: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomSecurityHubEnableOrganizationAdminAccountCustomResourceProviderRole1CBC866F'], + Properties: { + Code: { + S3Bucket: { + 'Fn::Sub': 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', + }, + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': [ + 'CustomSecurityHubEnableOrganizationAdminAccountCustomResourceProviderRole1CBC866F', + 'Arn', + ], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * EnableOrganizationAdminAccount custom resource iam role test + */ + test(`${testNamePrefix} EnableOrganizationAdminAccount custom resource iam role test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomSecurityHubEnableOrganizationAdminAccountCustomResourceProviderRole1CBC866F: { + Type: 'AWS::IAM::Role', + // UpdateReplacePolicy: 'Retain', + // DeletionPolicy: 'Retain', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: [ + 'organizations:DescribeOrganization', + 'organizations:ListAccounts', + 'organizations:ListDelegatedAdministrators', + ], + Effect: 'Allow', + Resource: '*', + Sid: 'SecurityHubEnableOrganizationAdminAccountTaskOrganizationActions', + }, + { + Action: 'organizations:EnableAWSServiceAccess', + Condition: { + StringEquals: { + 'organizations:ServicePrincipal': 'securityhub.amazonaws.com', + }, + }, + Effect: 'Allow', + Resource: '*', + }, + { + Action: [ + 'organizations:RegisterDelegatedAdministrator', + 'organizations:DeregisterDelegatedAdministrator', + ], + Condition: { + StringEquals: { + 'organizations:ServicePrincipal': 'securityhub.amazonaws.com', + }, + }, + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':organizations::*:account/o-*/*', + ], + ], + }, + }, + { + Action: ['iam:CreateServiceLinkedRole'], + Condition: { + StringLike: { + 'iam:AWSServiceName': ['securityhub.amazonaws.com'], + }, + }, + Effect: 'Allow', + Resource: '*', + Sid: 'SecurityHubCreateMembersTaskIamAction', + }, + { + Action: [ + 'securityhub:DisableOrganizationAdminAccount', + 'securityhub:EnableOrganizationAdminAccount', + 'securityhub:EnableSecurityHub', + 'securityhub:ListOrganizationAdminAccounts', + ], + Effect: 'Allow', + Resource: '*', + Sid: 'SecurityHubEnableOrganizationAdminAccountTaskSecurityHubActions', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * SecurityHubOrganizationAdminAccount custom resource test + */ + test(`${testNamePrefix} SecurityHubOrganizationAdminAccount custom resource test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + SecurityHubOrganizationAdminAccount71D5E029: { + Type: 'Custom::SecurityHubEnableOrganizationAdminAccount', + DeletionPolicy: 'Delete', + UpdateReplacePolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': [ + 'CustomSecurityHubEnableOrganizationAdminAccountCustomResourceProviderHandler194C30B9', + 'Arn', + ], + }, + adminAccountId: { + Ref: 'AWS::AccountId', + }, + region: { + Ref: 'AWS::Region', + }, + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-securityhub/securityhub-standards.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-securityhub/securityhub-standards.test.ts new file mode 100644 index 000000000..af0cf046f --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-securityhub/securityhub-standards.test.ts @@ -0,0 +1,184 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { SecurityHubStandards } from '../../index'; + +const testNamePrefix = 'Construct(SecurityHubStandards): '; + +//Initialize stack for snapshot test and resource configuration test +const stack = new cdk.Stack(); + +new SecurityHubStandards(stack, 'SecurityHubStandards', { + standards: [ + { + name: 'AWS Foundational Security Best Practices v1.0.0', + enable: true, + controlsToDisable: ['IAM.1', 'EC2.10', 'Lambda.4'], + }, + { + name: 'PCI DSS v3.2.1', + enable: true, + controlsToDisable: ['IAM.1', 'EC2.10', 'Lambda.4'], + }, + ], + kmsKey: new cdk.aws_kms.Key(stack, 'CustomKey', {}), + logRetentionInDays: 3653, +}); + +/** + * SecurityHubStandards construct test + */ +describe('SecurityHubStandards', () => { + /** + * Number of IAM role test + */ + test(`${testNamePrefix} IAM role count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 1); + }); + + /** + * Number of Lambda function test + */ + test(`${testNamePrefix} Lambda function count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 1); + }); + + /** + * Number of CustomResource test + */ + test(`${testNamePrefix} CustomResource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::SecurityHubBatchEnableStandards', 1); + }); + + /** + * SecurityHubStandards custom resource lambda function configuration test + */ + test(`${testNamePrefix} SecurityHubStandards custom resource lambda function configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomSecurityHubBatchEnableStandardsCustomResourceProviderHandler4BE622C1: { + Type: 'AWS::Lambda::Function', + DependsOn: ['CustomSecurityHubBatchEnableStandardsCustomResourceProviderRole1ABC8ED2'], + Properties: { + Code: { + S3Bucket: { + 'Fn::Sub': 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', + }, + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomSecurityHubBatchEnableStandardsCustomResourceProviderRole1ABC8ED2', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + }, + }, + }); + }); + + /** + * SecurityHubStandards custom resource iam role test + */ + test(`${testNamePrefix} SecurityHubStandards custom resource iam role test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomSecurityHubBatchEnableStandardsCustomResourceProviderRole1ABC8ED2: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: [ + 'securityhub:BatchDisableStandards', + 'securityhub:BatchEnableStandards', + 'securityhub:DescribeStandards', + 'securityhub:DescribeStandardsControls', + 'securityhub:EnableSecurityHub', + 'securityhub:GetEnabledStandards', + 'securityhub:UpdateStandardsControl', + ], + Effect: 'Allow', + Resource: '*', + Sid: 'SecurityHubCreateMembersTaskSecurityHubActions', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + }, + }, + }); + }); + + /** + * SecurityHubMembers custom resource test + */ + test(`${testNamePrefix} SecurityHubMembers custom resource test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + SecurityHubStandards294083BB: { + Type: 'Custom::SecurityHubBatchEnableStandards', + UpdateReplacePolicy: 'Delete', + DeletionPolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomSecurityHubBatchEnableStandardsCustomResourceProviderHandler4BE622C1', 'Arn'], + }, + region: { + Ref: 'AWS::Region', + }, + standards: [ + { + controlsToDisable: ['IAM.1', 'EC2.10', 'Lambda.4'], + enable: true, + name: 'AWS Foundational Security Best Practices v1.0.0', + }, + { + controlsToDisable: ['IAM.1', 'EC2.10', 'Lambda.4'], + enable: true, + name: 'PCI DSS v3.2.1', + }, + ], + }, + }, + }, + }); + }); + + //End of file +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-ssm/session-manager-settings.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-ssm/session-manager-settings.test.ts new file mode 100644 index 000000000..dbf75d880 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-ssm/session-manager-settings.test.ts @@ -0,0 +1,760 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { SsmSessionManagerSettings } from '../../index'; + +const testNamePrefix = 'Construct(SsmSessionManagerSettings): '; + +//Initialize stack for snapshot test and resource configuration test +const stack = new cdk.Stack(); + +new SsmSessionManagerSettings(stack, 'SsmSessionManagerSettings', { + s3BucketName: 'bucketName', + s3KeyPrefix: 'prefix', + s3BucketKeyArn: 'arn', + sendToS3: true, + sendToCloudWatchLogs: true, + cloudWatchEncryptionEnabled: true, + kmsKey: new cdk.aws_kms.Key(stack, 'Key', {}), + logRetentionInDays: 3653, +}); + +/** + * SsmSessionManagerSettings construct test + */ +describe('SsmSessionManagerSettings', () => { + /** + * Number of Lambda Function test + */ + test(`${testNamePrefix} Lambda Function count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 1); + }); + + /** + * Number of IAM Role test + */ + test(`${testNamePrefix} IAM Role count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 2); + }); + + /** + * Number of IAM Instance Profile + */ + test(`${testNamePrefix} IAM Instance Profile count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::InstanceProfile', 1); + }); + + /** + * Number of IAM Managed Policy + */ + test(`${testNamePrefix} IAM Managed Policy count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::ManagedPolicy', 2); + }); + + /** + * Number of KMS Key test + */ + test(`${testNamePrefix} KMS Key count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::KMS::Key', 3); + }); + + /** + * Number of KMS Alias test + */ + test(`${testNamePrefix} KMS Alias count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::KMS::Alias', 2); + }); + + /** + * Number of Log Groups test + */ + test(`${testNamePrefix} Log Group count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Logs::LogGroup', 2); + }); + + test(`${testNamePrefix} KMS Alias config test`, () => { + cdk.assertions.Template.fromStack(stack).hasResourceProperties('AWS::KMS::Alias', { + AliasName: 'alias/accelerator/session-manager-logging/cloud-watch-logs', + }); + }); + + test(`${testNamePrefix} KMS Alias config test`, () => { + cdk.assertions.Template.fromStack(stack).hasResourceProperties('AWS::KMS::Alias', { + AliasName: 'alias/accelerator/session-manager-logging/session', + }); + }); + + /** + * Number of Custom resource SsmSessionManagerSettings test + */ + test(`${testNamePrefix} Custom resource SsmSessionManagerSettings count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::SsmSessionManagerSettings', 1); + }); + + /** + * Custom resource provider framework lambda function configuration test + */ + test(`${testNamePrefix} Custom resource provider framework lambda function configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomSessionManagerLoggingCustomResourceProviderHandler4FE51699: { + DependsOn: ['CustomSessionManagerLoggingCustomResourceProviderRole1D8EE686'], + Properties: { + Code: { + S3Bucket: { + 'Fn::Sub': 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', + }, + }, + Handler: '__entrypoint__.handler', + MemorySize: 128, + Role: { + 'Fn::GetAtt': ['CustomSessionManagerLoggingCustomResourceProviderRole1D8EE686', 'Arn'], + }, + Runtime: 'nodejs14.x', + Timeout: 900, + }, + Type: 'AWS::Lambda::Function', + }, + }, + }); + }); + + /** + * Custom resource provider framework lambda function configuration test + */ + test(`${testNamePrefix} Custom resource provider framework lambda function configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + CustomSessionManagerLoggingCustomResourceProviderRole1D8EE686: { + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: ['ssm:DescribeDocument', 'ssm:CreateDocument', 'ssm:UpdateDocument'], + Effect: 'Allow', + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'Inline', + }, + ], + }, + Type: 'AWS::IAM::Role', + }, + }, + }); + }); + + /** + * Custom resource provider framework lambda function configuration test + */ + test(`${testNamePrefix} Custom resource provider framework lambda function configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + SsmSessionManagerSettings24721AC9: { + DeletionPolicy: 'Delete', + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomSessionManagerLoggingCustomResourceProviderHandler4FE51699', 'Arn'], + }, + cloudWatchEncryptionEnabled: true, + cloudWatchLogGroupName: { + Ref: 'SsmSessionManagerSettingssessionManagerLogGroupEA6AC1BB', + }, + kmsKeyId: { + Ref: 'SsmSessionManagerSettingsSessionManagerSessionCmk0B840C26', + }, + s3BucketName: 'bucketName', + s3EncryptionEnabled: true, + s3KeyPrefix: 'prefix', + }, + Type: 'Custom::SsmSessionManagerSettings', + UpdateReplacePolicy: 'Delete', + }, + }, + }); + }); + + /** + * Custom resource provider framework lambda function configuration test + */ + test(`${testNamePrefix} Custom resource provider framework lambda function configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + SsmSessionManagerSettingsSessionManagerEC2InstanceProfile36B87210: { + Properties: { + InstanceProfileName: { + 'Fn::Join': [ + '', + [ + 'SessionManagerEc2Role-', + { + Ref: 'AWS::Region', + }, + ], + ], + }, + Roles: [ + { + Ref: 'SsmSessionManagerSettingsSessionManagerEC2Role83702F06', + }, + ], + }, + Type: 'AWS::IAM::InstanceProfile', + }, + }, + }); + }); + + /** + * Custom resource provider framework lambda function configuration test + */ + test(`${testNamePrefix} Custom resource provider framework lambda function configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + SsmSessionManagerSettingsSessionManagerEC2Policy8ED295CA: { + Properties: { + Description: '', + ManagedPolicyName: { + 'Fn::Join': [ + '', + [ + 'SessionManagerLogging-', + { + Ref: 'AWS::Region', + }, + ], + ], + }, + Path: '/', + PolicyDocument: { + Statement: [ + { + Action: [ + 'ssmmessages:CreateControlChannel', + 'ssmmessages:CreateDataChannel', + 'ssmmessages:OpenControlChannel', + 'ssmmessages:OpenDataChannel', + 'ssm:UpdateInstanceInformation', + ], + Effect: 'Allow', + Resource: '*', + }, + { + Action: 'logs:DescribeLogGroups', + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':logs:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':log-group:*', + ], + ], + }, + }, + { + Action: ['logs:CreateLogStream', 'logs:PutLogEvents', 'logs:DescribeLogStreams'], + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':logs:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':log-group:aws-accelerator-session-manager-logs:*', + ], + ], + }, + }, + { + Action: ['s3:PutObject', 's3:PutObjectAcl'], + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':s3:::bucketName/prefix/*', + ], + ], + }, + }, + { + Action: 's3:GetEncryptionConfiguration', + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':s3:::bucketName', + ], + ], + }, + }, + { + Action: ['kms:Decrypt', 'kms:GenerateDataKey'], + Effect: 'Allow', + Resource: 'arn', + }, + { + Action: 'kms:Decrypt', + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': ['SsmSessionManagerSettingsSessionManagerSessionCmk0B840C26', 'Arn'], + }, + }, + ], + Version: '2012-10-17', + }, + }, + Type: 'AWS::IAM::ManagedPolicy', + }, + }, + }); + }); + + /** + * Custom resource provider framework lambda function configuration test + */ + test(`${testNamePrefix} Custom resource provider framework lambda function configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + SsmSessionManagerSettingsSessionManagerEC2Role83702F06: { + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: { + 'Fn::Join': [ + '', + [ + 'ec2.', + { + Ref: 'AWS::URLSuffix', + }, + ], + ], + }, + }, + }, + ], + Version: '2012-10-17', + }, + Description: 'IAM Role for an EC2 configured for Session Manager Logging', + ManagedPolicyArns: [ + { + Ref: 'SsmSessionManagerSettingsSessionManagerEC2Policy8ED295CA', + }, + ], + RoleName: { + 'Fn::Join': [ + '', + [ + 'SessionManagerEC2Role-', + { + Ref: 'AWS::Region', + }, + ], + ], + }, + }, + Type: 'AWS::IAM::Role', + }, + }, + }); + }); + + /** + * Custom resource provider framework lambda function configuration test + */ + test(`${testNamePrefix} Custom resource provider framework lambda function configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + SsmSessionManagerSettingsSessionManagerLogsCmkAlias739EF1F2: { + Properties: { + AliasName: 'alias/accelerator/session-manager-logging/cloud-watch-logs', + TargetKeyId: { + 'Fn::GetAtt': ['SsmSessionManagerSettingsSessionManagerLogsCmk796F4752', 'Arn'], + }, + }, + Type: 'AWS::KMS::Alias', + }, + }, + }); + }); + + /** + * Custom resource provider framework lambda function configuration test + */ + test(`${testNamePrefix} Custom resource provider framework lambda function configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + SsmSessionManagerSettingsSessionManagerLogsCmk796F4752: { + DeletionPolicy: 'Retain', + Properties: { + Description: 'AWS Accelerator Cloud Watch Logs CMK for Session Manager Logs', + EnableKeyRotation: true, + KeyPolicy: { + Statement: [ + { + Action: 'kms:*', + Effect: 'Allow', + Principal: { + AWS: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::', + { + Ref: 'AWS::AccountId', + }, + ':root', + ], + ], + }, + }, + Resource: '*', + }, + { + Action: 'kms:*', + Effect: 'Allow', + Principal: { + AWS: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::', + { + Ref: 'AWS::AccountId', + }, + ':root', + ], + ], + }, + }, + Resource: '*', + Sid: 'Enable IAM User Permissions', + }, + { + Action: ['kms:Encrypt*', 'kms:Decrypt*', 'kms:ReEncrypt*', 'kms:GenerateDataKey*', 'kms:Describe*'], + Condition: { + ArnLike: { + 'kms:EncryptionContext:aws:logs:arn': { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':logs:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':*', + ], + ], + }, + }, + }, + Effect: 'Allow', + Principal: { + Service: { + 'Fn::Join': [ + '', + [ + 'logs.', + { + Ref: 'AWS::Region', + }, + '.amazonaws.com', + ], + ], + }, + }, + Resource: '*', + Sid: 'Allow Cloud Watch Logs access', + }, + ], + Version: '2012-10-17', + }, + }, + Type: 'AWS::KMS::Key', + UpdateReplacePolicy: 'Retain', + }, + }, + }); + }); + + /** + * Custom resource provider framework lambda function configuration test + */ + test(`${testNamePrefix} Custom resource provider framework lambda function configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + SsmSessionManagerSettingsSessionManagerSessionCmkAliasC8D340F2: { + Properties: { + AliasName: 'alias/accelerator/session-manager-logging/session', + TargetKeyId: { + 'Fn::GetAtt': ['SsmSessionManagerSettingsSessionManagerSessionCmk0B840C26', 'Arn'], + }, + }, + Type: 'AWS::KMS::Alias', + }, + }, + }); + }); + + /** + * Custom resource provider framework lambda function configuration test + */ + test(`${testNamePrefix} Custom resource provider framework lambda function configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + SsmSessionManagerSettingsSessionManagerSessionCmk0B840C26: { + DeletionPolicy: 'Retain', + Properties: { + Description: 'AWS Accelerator Cloud Watch Logs CMK for Session Manager Logs', + EnableKeyRotation: true, + KeyPolicy: { + Statement: [ + { + Action: 'kms:*', + Effect: 'Allow', + Principal: { + AWS: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::', + { + Ref: 'AWS::AccountId', + }, + ':root', + ], + ], + }, + }, + Resource: '*', + }, + { + Action: 'kms:*', + Effect: 'Allow', + Principal: { + AWS: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::', + { + Ref: 'AWS::AccountId', + }, + ':root', + ], + ], + }, + }, + Resource: '*', + Sid: 'Enable IAM User Permissions', + }, + { + Action: ['kms:Encrypt*', 'kms:Decrypt*', 'kms:ReEncrypt*', 'kms:GenerateDataKey*', 'kms:Describe*'], + Condition: { + ArnLike: { + 'kms:EncryptionContext:aws:logs:arn': { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':logs:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':*', + ], + ], + }, + }, + }, + Effect: 'Allow', + Principal: { + Service: { + 'Fn::Join': [ + '', + [ + 'logs.', + { + Ref: 'AWS::Region', + }, + '.amazonaws.com', + ], + ], + }, + }, + Resource: '*', + Sid: 'Allow Cloud Watch Logs access', + }, + ], + Version: '2012-10-17', + }, + }, + Type: 'AWS::KMS::Key', + UpdateReplacePolicy: 'Retain', + }, + }, + }); + }); + + /** + * Custom resource provider framework lambda function configuration test + */ + test(`${testNamePrefix} Custom resource provider framework lambda function configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + SsmSessionManagerSettingsSessionManagerUserKMSPolicyFB96BB42: { + Properties: { + Description: '', + ManagedPolicyName: { + 'Fn::Join': [ + '', + [ + 'SessionManagerUserKMSPolicy-', + { + Ref: 'AWS::Region', + }, + ], + ], + }, + Path: '/', + PolicyDocument: { + Statement: [ + { + Action: ['kms:Decrypt', 'kms:GenerateDataKey'], + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': ['SsmSessionManagerSettingsSessionManagerSessionCmk0B840C26', 'Arn'], + }, + }, + ], + Version: '2012-10-17', + }, + }, + Type: 'AWS::IAM::ManagedPolicy', + }, + }, + }); + }); + + /** + * Custom resource provider framework lambda function configuration test + */ + test(`${testNamePrefix} Custom resource provider framework lambda function configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + SsmSessionManagerSettingssessionManagerLogGroupEA6AC1BB: { + DeletionPolicy: 'Retain', + Properties: { + KmsKeyId: { + 'Fn::GetAtt': ['SsmSessionManagerSettingsSessionManagerLogsCmk796F4752', 'Arn'], + }, + LogGroupName: 'aws-accelerator-session-manager-logs', + RetentionInDays: 3653, + }, + Type: 'AWS::Logs::LogGroup', + UpdateReplacePolicy: 'Retain', + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/test/aws-ssm/ssm-parameter-lookup.test.ts b/source/packages/@aws-accelerator/constructs/test/aws-ssm/ssm-parameter-lookup.test.ts new file mode 100644 index 000000000..fee6efec7 --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/test/aws-ssm/ssm-parameter-lookup.test.ts @@ -0,0 +1,94 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { SsmParameterLookup } from '../../index'; + +const testNamePrefix = 'Construct(SsmParameterLookup): '; + +//Initialize stack for snapshot test and resource configuration test +const stack = new cdk.Stack(); + +new SsmParameterLookup(stack, 'SsmParameter', { + name: 'TestParameter', + accountId: '123123123123', + roleName: 'TestRole', + logRetentionInDays: 3653, +}); + +/** + * SsmParameterLookup construct test + */ +describe('SsmParameterLookup', () => { + /** + * Number of Lambda Function test + */ + test(`${testNamePrefix} Lambda Function count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 1); + }); + + /** + * Number of IAM Role test + */ + test(`${testNamePrefix} IAM Role count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 1); + }); + + /** + * Number of Custom resource SsmGetParameterValue test + */ + test(`${testNamePrefix} Custom resource SsmGetParameterValue count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('Custom::SsmGetParameterValue', 1); + }); + + /** + * Custom resource SsmParameterLookup configuration test + */ + test(`${testNamePrefix} Custom resource SsmParameter configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + SsmParameter39B3125C: { + Type: 'Custom::SsmGetParameterValue', + UpdateReplacePolicy: 'Delete', + DeletionPolicy: 'Delete', + DependsOn: ['CustomSsmGetParameterValueCustomResourceProviderLogGroup780D220D'], + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomSsmGetParameterValueCustomResourceProviderHandlerAAD0E7EE', 'Arn'], + }, + assumeRoleArn: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::123123123123:role/TestRole', + ], + ], + }, + invokingAccountID: { + Ref: 'AWS::AccountId', + }, + parameterAccountID: '123123123123', + parameterName: 'TestParameter', + region: { + Ref: 'AWS::Region', + }, + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/constructs/tsconfig.json b/source/packages/@aws-accelerator/constructs/tsconfig.json new file mode 100644 index 000000000..7367be01c --- /dev/null +++ b/source/packages/@aws-accelerator/constructs/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": {}, + "include": ["lib/*/*.ts", "index.ts"], + "exclude": ["test/**/*"] +} diff --git a/source/packages/@aws-accelerator/installer/.npmignore b/source/packages/@aws-accelerator/installer/.npmignore new file mode 100644 index 000000000..c1d6d45dc --- /dev/null +++ b/source/packages/@aws-accelerator/installer/.npmignore @@ -0,0 +1,6 @@ +*.ts +!*.d.ts + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/source/packages/@aws-accelerator/installer/README.md b/source/packages/@aws-accelerator/installer/README.md new file mode 100644 index 000000000..ce42ade0f --- /dev/null +++ b/source/packages/@aws-accelerator/installer/README.md @@ -0,0 +1 @@ +# @aws-accelerator/installer diff --git a/source/packages/@aws-accelerator/installer/bin/installer.ts b/source/packages/@aws-accelerator/installer/bin/installer.ts new file mode 100644 index 000000000..9c3200a2b --- /dev/null +++ b/source/packages/@aws-accelerator/installer/bin/installer.ts @@ -0,0 +1,43 @@ +#!/usr/bin/env node + +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import 'source-map-support/register'; +import * as installer from '../lib/installer-stack'; +import { AwsSolutionsChecks } from 'cdk-nag'; + +const app = new cdk.App(); +cdk.Aspects.of(app).add(new AwsSolutionsChecks()); + +const useExternalPipelineAccount = app.node.tryGetContext('use-external-pipeline-account') === 'true'; +const enableTester = app.node.tryGetContext('enable-tester') === 'true'; +const managementCrossAccountRoleName = app.node.tryGetContext('management-cross-account-role-name'); + +if (enableTester && managementCrossAccountRoleName === undefined) { + console.log(`Invalid --management-cross-account-role-name ${managementCrossAccountRoleName}`); + throw new Error( + 'Usage: app.ts [--context use-external-pipeline-account=BOOLEAN] [--context enable-tester=BOOLEAN] [--context management-cross-account-role-name=MANAGEMENT_CROSS_ACCOUNT_ROLE_NAME]', + ); +} + +new installer.InstallerStack(app, 'AWSAccelerator-InstallerStack', { + description: `(SO0199) Landing Zone Accelerator on AWS`, + synthesizer: new cdk.DefaultStackSynthesizer({ + generateBootstrapVersionRule: false, + }), + useExternalPipelineAccount: useExternalPipelineAccount, + enableTester: enableTester, + managementCrossAccountRoleName: managementCrossAccountRoleName, +}); diff --git a/source/packages/@aws-accelerator/installer/cdk.json b/source/packages/@aws-accelerator/installer/cdk.json new file mode 100644 index 000000000..a56f18e88 --- /dev/null +++ b/source/packages/@aws-accelerator/installer/cdk.json @@ -0,0 +1,5 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/installer.ts", + "versionReporting": false, + "context": {} +} diff --git a/source/packages/@aws-accelerator/installer/index.ts b/source/packages/@aws-accelerator/installer/index.ts new file mode 100644 index 000000000..b67af4889 --- /dev/null +++ b/source/packages/@aws-accelerator/installer/index.ts @@ -0,0 +1,14 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +export * from './lib/installer-stack'; diff --git a/source/packages/@aws-accelerator/installer/jest.config.js b/source/packages/@aws-accelerator/installer/jest.config.js new file mode 100644 index 000000000..0e8a6842d --- /dev/null +++ b/source/packages/@aws-accelerator/installer/jest.config.js @@ -0,0 +1,6 @@ +const base = require('../../../jest.config.base'); +const packageJson = require('./package.json'); + +module.exports = { + ...base.getJestJunitConfig(packageJson.name), +}; diff --git a/source/packages/@aws-accelerator/installer/lib/installer-stack.ts b/source/packages/@aws-accelerator/installer/lib/installer-stack.ts new file mode 100644 index 000000000..1d7dfe362 --- /dev/null +++ b/source/packages/@aws-accelerator/installer/lib/installer-stack.ts @@ -0,0 +1,743 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { NagSuppressions } from 'cdk-nag'; +import { Construct } from 'constructs'; + +import { Bucket, BucketEncryptionType } from '@aws-accelerator/constructs'; +import { SolutionHelper } from './solutions-helper'; + +import { version } from '../../../../package.json'; + +export enum RepositorySources { + GITHUB = 'github', + CODECOMMIT = 'codecommit', +} + +export interface InstallerStackProps extends cdk.StackProps { + /** + * External Pipeline Account usage flag + */ + readonly useExternalPipelineAccount: boolean; + /** + * Enable tester flag + */ + readonly enableTester: boolean; + + /** + * Management Cross account role name + */ + readonly managementCrossAccountRoleName?: string; +} + +export class InstallerStack extends cdk.Stack { + // TODO: Add allowedPattern for all CfnParameter uses + private readonly repositorySource = new cdk.CfnParameter(this, 'RepositorySource', { + type: 'String', + description: 'Specify the git host', + allowedValues: [RepositorySources.GITHUB, RepositorySources.CODECOMMIT], + default: RepositorySources.GITHUB, + }); + + private readonly repositoryOwner = new cdk.CfnParameter(this, 'RepositoryOwner', { + type: 'String', + description: 'The owner of the repository containing the accelerator code. (GitHub Only)', + default: 'awslabs', + }); + + private readonly repositoryName = new cdk.CfnParameter(this, 'RepositoryName', { + type: 'String', + description: 'The name of the git repository hosting the accelerator code', + default: 'landing-zone-accelerator-on-aws', + }); + + private readonly repositoryBranchName = new cdk.CfnParameter(this, 'RepositoryBranchName', { + type: 'String', + description: 'The name of the git branch to use for installation', + }); + + private readonly enableApprovalStage = new cdk.CfnParameter(this, 'EnableApprovalStage', { + type: 'String', + description: 'Select yes to add a Manual Approval stage to accelerator pipeline', + allowedValues: ['Yes', 'No'], + default: 'Yes', + }); + + private readonly approvalStageNotifyEmailList = new cdk.CfnParameter(this, 'ApprovalStageNotifyEmailList', { + type: 'CommaDelimitedList', + description: 'Provide comma(,) separated list of email ids to receive manual approval stage notification email', + }); + + private readonly managementAccountEmail = new cdk.CfnParameter(this, 'ManagementAccountEmail', { + type: 'String', + description: 'The management (primary) account email', + }); + + private readonly logArchiveAccountEmail = new cdk.CfnParameter(this, 'LogArchiveAccountEmail', { + type: 'String', + description: 'The log archive account email', + }); + + private readonly auditAccountEmail = new cdk.CfnParameter(this, 'AuditAccountEmail', { + type: 'String', + description: 'The security audit account (also referred to as the audit account)', + }); + + /** + * Management Account ID Parameter + * @private + */ + private readonly managementAccountId: cdk.CfnParameter | undefined; + + /** + * Management Account Role Name Parameter + * @private + */ + private readonly managementAccountRoleName: cdk.CfnParameter | undefined; + + /** + * Accelerator Qualifier parameter + * @private + */ + private readonly acceleratorQualifier: cdk.CfnParameter | undefined; + + constructor(scope: Construct, id: string, props: InstallerStackProps) { + super(scope, id, props); + + const isCommercialCondition = new cdk.CfnCondition(this, 'IsCommercialCondition', { + expression: cdk.Fn.conditionEquals(cdk.Stack.of(this).partition, 'aws'), + }); + + const globalRegionMap = new cdk.CfnMapping(this, 'GlobalRegionMap', { + mapping: { + aws: { + regionName: 'us-east-1', + }, + 'aws-us-gov': { + regionName: 'us-gov-west-1', + }, + 'aws-iso-b': { + regionName: 'us-isob-east-1', + }, + 'aws-iso': { + regionName: 'us-iso-east-1', + }, + }, + }); + + const parameterGroups: { Label: { default: string }; Parameters: string[] }[] = [ + { + Label: { default: 'Git Repository Configuration' }, + Parameters: [ + this.repositorySource.logicalId, + this.repositoryOwner.logicalId, + this.repositoryName.logicalId, + this.repositoryBranchName.logicalId, + ], + }, + { + Label: { default: 'Pipeline Configuration' }, + Parameters: [this.enableApprovalStage.logicalId, this.approvalStageNotifyEmailList.logicalId], + }, + { + Label: { default: 'Mandatory Accounts Configuration' }, + Parameters: [ + this.managementAccountEmail.logicalId, + this.logArchiveAccountEmail.logicalId, + this.auditAccountEmail.logicalId, + ], + }, + ]; + + const repositoryParameterLabels: { [p: string]: { default: string } } = { + [this.repositorySource.logicalId]: { default: 'Source' }, + [this.repositoryOwner.logicalId]: { default: 'Repository Owner' }, + [this.repositoryName.logicalId]: { default: 'Repository Name' }, + [this.repositoryBranchName.logicalId]: { default: 'Branch Name' }, + [this.enableApprovalStage.logicalId]: { default: 'Enable Approval Stage' }, + [this.approvalStageNotifyEmailList.logicalId]: { default: 'Manual Approval Stage notification email list' }, + [this.managementAccountEmail.logicalId]: { default: 'Management Account Email' }, + [this.logArchiveAccountEmail.logicalId]: { default: 'Log Archive Account Email' }, + [this.auditAccountEmail.logicalId]: { default: 'Audit Account Email' }, + }; + + let targetAcceleratorParameterLabels: { [p: string]: { default: string } } = {}; + let targetAcceleratorEnvVariables: { [p: string]: cdk.aws_codebuild.BuildEnvironmentVariable } | undefined; + + if (props.useExternalPipelineAccount) { + this.acceleratorQualifier = new cdk.CfnParameter(this, 'AcceleratorQualifier', { + type: 'String', + description: 'Accelerator assets arn qualifier', + allowedPattern: '^[a-z]+[a-z0-9-]{1,61}[a-z0-9]+$', + }); + + this.managementAccountId = new cdk.CfnParameter(this, 'ManagementAccountId', { + type: 'String', + description: 'Target management account id', + }); + + this.managementAccountRoleName = new cdk.CfnParameter(this, 'ManagementAccountRoleName', { + type: 'String', + description: 'Target management account role name', + }); + + parameterGroups.push({ + Label: { default: 'Target Environment Configuration' }, + Parameters: [ + this.acceleratorQualifier.logicalId, + this.managementAccountId.logicalId, + this.managementAccountRoleName.logicalId, + ], + }); + + targetAcceleratorParameterLabels = { + [this.acceleratorQualifier.logicalId]: { default: 'Accelerator Qualifier' }, + [this.managementAccountId.logicalId]: { default: 'Management Account ID' }, + [this.managementAccountRoleName.logicalId]: { default: 'Management Account Role Name' }, + }; + + targetAcceleratorEnvVariables = { + MANAGEMENT_ACCOUNT_ID: { + type: cdk.aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: this.managementAccountId.valueAsString, + }, + MANAGEMENT_ACCOUNT_ROLE_NAME: { + type: cdk.aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: this.managementAccountRoleName.valueAsString, + }, + ACCELERATOR_QUALIFIER: { + type: cdk.aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: this.acceleratorQualifier.valueAsString, + }, + }; + } + + let targetAcceleratorTestEnvVariables: { [p: string]: cdk.aws_codebuild.BuildEnvironmentVariable } | undefined; + if (props.enableTester) { + targetAcceleratorTestEnvVariables = { + ENABLE_TESTER: { + type: cdk.aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: props.enableTester, + }, + MANAGEMENT_CROSS_ACCOUNT_ROLE_NAME: { + type: cdk.aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: props.managementCrossAccountRoleName, + }, + }; + } + + // Parameter Metadata + this.templateOptions.metadata = { + 'AWS::CloudFormation::Interface': { + ParameterGroups: parameterGroups, + ParameterLabels: { ...repositoryParameterLabels, ...targetAcceleratorParameterLabels }, + }, + }; + + new cdk.aws_ssm.StringParameter(this, 'SsmParamStackId', { + parameterName: this.acceleratorQualifier + ? `/accelerator/${this.acceleratorQualifier.valueAsString}/${cdk.Stack.of(this).stackName}/stack-id` + : `/accelerator/${cdk.Stack.of(this).stackName}/stack-id`, + stringValue: cdk.Stack.of(this).stackId, + simpleName: false, + }); + + new cdk.aws_ssm.StringParameter(this, 'SsmParamAcceleratorVersion', { + parameterName: this.acceleratorQualifier + ? `/accelerator/${this.acceleratorQualifier.valueAsString}/${cdk.Stack.of(this).stackName}/version` + : `/accelerator/${cdk.Stack.of(this).stackName}/version`, + stringValue: version, + simpleName: false, + }); + + /** + * Solutions Metrics + * We use this data to better understand how customers use this + * solution and related services and products + */ + new SolutionHelper(this, 'SolutionHelper', { + solutionId: 'SO0199', + repositorySource: this.repositorySource, + repositoryOwner: this.repositoryOwner, + repositoryBranchName: this.repositoryBranchName, + repositoryName: this.repositoryName, + }); + + // Create Accelerator Installer KMS Key + const installerKey = new cdk.aws_kms.Key(this, 'InstallerKey', { + alias: this.acceleratorQualifier + ? `alias/accelerator/${this.acceleratorQualifier.valueAsString}/installer/kms/key` + : 'alias/accelerator/installer/kms/key', + description: 'AWS Accelerator Management Account Kms Key', + enableKeyRotation: true, + policy: undefined, + }); + + // + // Add conditional policies to Key policy + const cfnKey = installerKey.node.defaultChild as cdk.aws_kms.CfnKey; + cfnKey.keyPolicy = { + Statement: [ + { + Effect: 'Allow', + Principal: { + AWS: `arn:${cdk.Stack.of(this).partition}:iam::${cdk.Stack.of(this).account}:root`, + }, + Action: 'kms:*', + Resource: '*', + }, + { + Sid: 'Allow Accelerator Role to use the encryption key', + Effect: 'Allow', + Principal: { + AWS: '*', + }, + Action: ['kms:Encrypt', 'kms:Decrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*', 'kms:DescribeKey'], + Resource: '*', + Condition: { + ArnLike: { + 'aws:PrincipalARN': `arn:${cdk.Stack.of(this).partition}:iam::${cdk.Stack.of(this).account}:role/${ + this.acceleratorQualifier ? this.acceleratorQualifier.valueAsString : 'AWSAccelerator' + }-*`, + }, + }, + }, + { + Sid: 'Allow SNS service to use the encryption key', + Effect: 'Allow', + Principal: { + Service: 'sns.amazonaws.com', + }, + Action: ['kms:Encrypt', 'kms:Decrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*', 'kms:DescribeKey'], + Resource: '*', + }, + cdk.Fn.conditionIf( + isCommercialCondition.logicalId, + { + Sid: 'KMS key access to codestar-notifications', + Effect: 'Allow', + Principal: { + Service: 'codestar-notifications.amazonaws.com', + }, + Action: ['kms:GenerateDataKey*', 'kms:Decrypt'], + Resource: '*', + Condition: { + StringEquals: { + 'kms:ViaService': `sns.${cdk.Stack.of(this).region}.amazonaws.com`, + }, + }, + }, + cdk.Aws.NO_VALUE, + ), + ], + }; + + // cfn_nag suppressions + const cfnInstallerKey = installerKey.node.defaultChild as cdk.aws_kms.CfnKey; + cfnInstallerKey.cfnOptions.metadata = { + cfn_nag: { + rules_to_suppress: [ + { + id: 'F76', + reason: 'KMS key using * principal with added arn condition', + }, + ], + }, + }; + + // Create SSM parameter for installer key arn for future use + new cdk.aws_ssm.StringParameter(this, 'AcceleratorManagementKmsArnParameter', { + parameterName: this.acceleratorQualifier + ? `/accelerator/${this.acceleratorQualifier.valueAsString}/installer/kms/key-arn` + : '/accelerator/installer/kms/key-arn', + stringValue: installerKey.keyArn, + simpleName: false, + }); + + const installerServerAccessLogsBucket = new Bucket(this, 'InstallerAccessLogsBucket', { + encryptionType: BucketEncryptionType.SSE_S3, // Server access logging does not support SSE-KMS + s3BucketName: `${ + this.acceleratorQualifier ? this.acceleratorQualifier.valueAsString : 'aws-accelerator' + }-s3-logs-${cdk.Aws.ACCOUNT_ID}-${cdk.Aws.REGION}`, + }); + + // cfn_nag: Suppress warning related to high S3 Bucket should have access logging configured + const cfnInstallerServerAccessLogsBucket = installerServerAccessLogsBucket.getS3Bucket().node + .defaultChild as cdk.aws_s3.CfnBucket; + cfnInstallerServerAccessLogsBucket.cfnOptions.metadata = { + cfn_nag: { + rules_to_suppress: [ + { + id: 'W35', + reason: 'This is an access logging bucket.', + }, + ], + }, + }; + + // AwsSolutions-S1: The S3 Bucket has server access logs disabled. + NagSuppressions.addResourceSuppressionsByPath( + this, + `${this.stackName}/InstallerAccessLogsBucket/Resource/Resource`, + [ + { + id: 'AwsSolutions-S1', + reason: 'AccessLogsBucket has server access logs disabled till the task for access logging completed.', + }, + ], + ); + + new cdk.aws_ssm.StringParameter(this, 'InstallerAccessLogsBucketName', { + parameterName: this.acceleratorQualifier + ? `/accelerator/${this.acceleratorQualifier.valueAsString}/installer-access-logs-bucket-name` + : `/accelerator/installer-access-logs-bucket-name`, + stringValue: installerServerAccessLogsBucket.getS3Bucket().bucketName, + simpleName: false, + }); + + const bucket = new Bucket(this, 'SecureBucket', { + encryptionType: BucketEncryptionType.SSE_KMS, + s3BucketName: `${ + this.acceleratorQualifier ? this.acceleratorQualifier.valueAsString : 'aws-accelerator' + }-installer-${cdk.Aws.ACCOUNT_ID}-${cdk.Aws.REGION}`, //TO DO change the bucket name + kmsKey: installerKey, + serverAccessLogsBucket: installerServerAccessLogsBucket.getS3Bucket(), + }); + + const installerRole = new cdk.aws_iam.Role(this, 'InstallerAdminRole', { + assumedBy: new cdk.aws_iam.ServicePrincipal('codebuild.amazonaws.com'), + // TODO: Lock this down to just the pipeline and cloudformation actions needed + managedPolicies: [cdk.aws_iam.ManagedPolicy.fromAwsManagedPolicyName('AdministratorAccess')], + }); + + const globalRegion = globalRegionMap.findInMap(cdk.Aws.PARTITION, 'regionName'); + + const installerProject = new cdk.aws_codebuild.PipelineProject(this, 'InstallerProject', { + projectName: this.acceleratorQualifier + ? `${this.acceleratorQualifier.valueAsString}-installer-project` + : 'AWSAccelerator-InstallerProject', + encryptionKey: installerKey, + role: installerRole, + buildSpec: cdk.aws_codebuild.BuildSpec.fromObjectToYaml({ + version: '0.2', + phases: { + install: { + 'runtime-versions': { + nodejs: 14, + }, + }, + pre_build: { + commands: [ + 'ENABLE_EXTERNAL_PIPELINE_ACCOUNT="no"', + 'if [ ! -z "$MANAGEMENT_ACCOUNT_ID" ] && [ ! -z "$MANAGEMENT_ACCOUNT_ROLE_NAME" ]; then ' + + 'ENABLE_EXTERNAL_PIPELINE_ACCOUNT="yes"; ' + + 'fi', + ], + }, + build: { + commands: [ + 'cd source', + 'yarn install', + 'yarn lerna link', + 'yarn build', + 'cd packages/@aws-accelerator/installer', + `yarn run cdk bootstrap --toolkitStackName AWSAccelerator-CDKToolkit aws://${cdk.Aws.ACCOUNT_ID}/${cdk.Aws.REGION} --qualifier accel`, + `yarn run cdk bootstrap --toolkitStackName AWSAccelerator-CDKToolkit aws://${cdk.Aws.ACCOUNT_ID}/${globalRegion} --qualifier accel`, + `if [ $ENABLE_EXTERNAL_PIPELINE_ACCOUNT = "yes" ]; then + export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s" $(aws sts assume-role --role-arn arn:${ + cdk.Stack.of(this).partition + }:iam::"$MANAGEMENT_ACCOUNT_ID":role/"$MANAGEMENT_ACCOUNT_ROLE_NAME" --role-session-name acceleratorAssumeRoleSession --query "Credentials.[AccessKeyId,SecretAccessKey,SessionToken]" --output text)); + yarn run cdk bootstrap --toolkitStackName AWSAccelerator-CDKToolkit aws://$MANAGEMENT_ACCOUNT_ID/${ + cdk.Aws.REGION + } --qualifier accel; + yarn run cdk bootstrap --toolkitStackName AWSAccelerator-CDKToolkit aws://$MANAGEMENT_ACCOUNT_ID/${globalRegion} --qualifier accel; + unset AWS_ACCESS_KEY_ID; + unset AWS_SECRET_ACCESS_KEY; + unset AWS_SESSION_TOKEN; + fi`, + 'cd ../accelerator', + `yarn run ts-node --transpile-only cdk.ts deploy --require-approval never --stage pipeline --account ${cdk.Aws.ACCOUNT_ID} --region ${cdk.Aws.REGION} --partition ${cdk.Aws.PARTITION}`, + `if [ "$ENABLE_TESTER" = "true" ]; then yarn run ts-node --transpile-only cdk.ts deploy --require-approval never --stage tester-pipeline --account ${cdk.Aws.ACCOUNT_ID} --region ${cdk.Aws.REGION}; fi`, + ], + }, + }, + }), + environment: { + buildImage: cdk.aws_codebuild.LinuxBuildImage.STANDARD_5_0, + privileged: true, // Allow access to the Docker daemon + computeType: cdk.aws_codebuild.ComputeType.MEDIUM, + environmentVariables: { + NODE_OPTIONS: { + type: cdk.aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: '--max_old_space_size=4096', + }, + CDK_NEW_BOOTSTRAP: { + type: cdk.aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: '1', + }, + ACCELERATOR_REPOSITORY_SOURCE: { + type: cdk.aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: this.repositorySource.valueAsString, + }, + ACCELERATOR_REPOSITORY_OWNER: { + type: cdk.aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: this.repositoryOwner.valueAsString, + }, + ACCELERATOR_REPOSITORY_NAME: { + type: cdk.aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: this.repositoryName.valueAsString, + }, + ACCELERATOR_REPOSITORY_BRANCH_NAME: { + type: cdk.aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: this.repositoryBranchName.valueAsString, + }, + ACCELERATOR_ENABLE_APPROVAL_STAGE: { + type: cdk.aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: this.enableApprovalStage.valueAsString, + }, + APPROVAL_STAGE_NOTIFY_EMAIL_LIST: { + type: cdk.aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: cdk.Fn.join(',', this.approvalStageNotifyEmailList.valueAsList), + }, + MANAGEMENT_ACCOUNT_EMAIL: { + type: cdk.aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: this.managementAccountEmail.valueAsString, + }, + LOG_ARCHIVE_ACCOUNT_EMAIL: { + type: cdk.aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: this.logArchiveAccountEmail.valueAsString, + }, + AUDIT_ACCOUNT_EMAIL: { + type: cdk.aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: this.auditAccountEmail.valueAsString, + }, + ...targetAcceleratorEnvVariables, + ...targetAcceleratorTestEnvVariables, + }, + }, + }); + + /** + * Pipeline + */ + const acceleratorRepoArtifact = new cdk.aws_codepipeline.Artifact('Source'); + + /** + * CodeCommit Pipeline + */ + + const codeCommitPipelineRole = new cdk.aws_iam.Role(this, 'CodeCommitPipelineRole', { + assumedBy: new cdk.aws_iam.ServicePrincipal('codepipeline.amazonaws.com'), + }); + + const codeCommitPipeline = new cdk.aws_codepipeline.Pipeline(this, 'CodeCommitPipeline', { + pipelineName: this.acceleratorQualifier + ? `${this.acceleratorQualifier.valueAsString}-installer` + : 'AWSAccelerator-Installer', + artifactBucket: bucket.getS3Bucket(), + restartExecutionOnUpdate: true, + role: codeCommitPipelineRole, + }); + + codeCommitPipeline.addStage({ + stageName: 'Source', + actions: [ + new cdk.aws_codepipeline_actions.CodeCommitSourceAction({ + actionName: 'Source', + repository: cdk.aws_codecommit.Repository.fromRepositoryName( + this, + 'SourceRepo', + this.repositoryName.valueAsString, + ), + branch: this.repositoryBranchName.valueAsString, + output: acceleratorRepoArtifact, + trigger: cdk.aws_codepipeline_actions.CodeCommitTrigger.NONE, + }), + ], + }); + + codeCommitPipeline.addStage({ + stageName: 'Install', + actions: [ + new cdk.aws_codepipeline_actions.CodeBuildAction({ + actionName: 'Install', + project: installerProject, + input: acceleratorRepoArtifact, + role: codeCommitPipelineRole, + }), + ], + }); + + const useCodeCommitCondition = new cdk.CfnCondition(this, 'UseCodeCommitCondition', { + expression: cdk.Fn.conditionEquals(this.repositorySource.valueAsString, RepositorySources.CODECOMMIT), + }); + + const cfnCodeCommitPipelinePolicy = codeCommitPipelineRole.node.findChild('DefaultPolicy').node + .defaultChild as cdk.aws_iam.CfnPolicy; + cfnCodeCommitPipelinePolicy.cfnOptions.condition = useCodeCommitCondition; + + const cfnCodeCommitPipelineRole = codeCommitPipelineRole.node.defaultChild as cdk.aws_iam.CfnRole; + cfnCodeCommitPipelineRole.cfnOptions.condition = useCodeCommitCondition; + + const cfnCodeCommitPipeline = codeCommitPipeline.node.defaultChild as cdk.aws_codepipeline.CfnPipeline; + cfnCodeCommitPipeline.cfnOptions.condition = useCodeCommitCondition; + + const cfnCodeCommitPipelineSource = codeCommitPipeline.node + .findChild('Source') + .node.findChild('Source') + .node.findChild('CodePipelineActionRole').node; + (cfnCodeCommitPipelineSource.defaultChild as cdk.CfnResource).cfnOptions.condition = useCodeCommitCondition; + (cfnCodeCommitPipelineSource.findChild('DefaultPolicy').node.defaultChild as cdk.CfnResource).cfnOptions.condition = + useCodeCommitCondition; + + /** + * GitHub Pipeline + */ + const gitHubPipelineRole = new cdk.aws_iam.Role(this, 'GitHubPipelineRole', { + assumedBy: new cdk.aws_iam.ServicePrincipal('codepipeline.amazonaws.com'), + }); + + const gitHubPipeline = new cdk.aws_codepipeline.Pipeline(this, 'GitHubPipeline', { + pipelineName: this.acceleratorQualifier + ? `${this.acceleratorQualifier.valueAsString}-installer` + : 'AWSAccelerator-Installer', + artifactBucket: bucket.getS3Bucket(), + restartExecutionOnUpdate: true, + role: gitHubPipelineRole, + }); + + gitHubPipeline.addStage({ + stageName: 'Source', + actions: [ + new cdk.aws_codepipeline_actions.GitHubSourceAction({ + actionName: 'Source', + owner: this.repositoryOwner.valueAsString, + repo: this.repositoryName.valueAsString, + branch: this.repositoryBranchName.valueAsString, + oauthToken: cdk.SecretValue.secretsManager('accelerator/github-token'), + output: acceleratorRepoArtifact, + trigger: cdk.aws_codepipeline_actions.GitHubTrigger.NONE, + }), + ], + }); + + gitHubPipeline.addStage({ + stageName: 'Install', + actions: [ + new cdk.aws_codepipeline_actions.CodeBuildAction({ + actionName: 'Install', + project: installerProject, + input: acceleratorRepoArtifact, + role: gitHubPipelineRole, + }), + ], + }); + + const useGitHubCondition = new cdk.CfnCondition(this, 'UseGitHubCondition', { + expression: cdk.Fn.conditionEquals(this.repositorySource.valueAsString, RepositorySources.GITHUB), + }); + + const cfnGitHubPipelinePolicy = gitHubPipelineRole.node.findChild('DefaultPolicy').node + .defaultChild as cdk.aws_iam.CfnPolicy; + cfnGitHubPipelinePolicy.cfnOptions.condition = useGitHubCondition; + + const cfnGitHubPipelineRole = gitHubPipelineRole.node.defaultChild as cdk.aws_iam.CfnRole; + cfnGitHubPipelineRole.cfnOptions.condition = useGitHubCondition; + + const cfnGitHubPipeline = gitHubPipeline.node.defaultChild as cdk.aws_codepipeline.CfnPipeline; + cfnGitHubPipeline.cfnOptions.condition = useGitHubCondition; + + // + // cdk-nag suppressions + // + + // [Error at /AWSAccelerator-InstallerStack/SecureBucket/Resource/Resource] + // AwsSolutions-S1: The S3 Bucket has server access logs disabled. + NagSuppressions.addResourceSuppressionsByPath( + this, + 'AWSAccelerator-InstallerStack/SecureBucket/Resource/Resource', + [ + { + id: 'AwsSolutions-S1', + reason: 'S3 Bucket access logging is not enabled for the pipeline artifacts bucket.', + }, + ], + ); + + // [Error at /AWSAccelerator-InstallerStack/GitHubPipelineRole/DefaultPolicy/Resource] + // AwsSolutions-IAM5: The IAM entity contains wildcard permissions and does not have a cdk_nag + // rule suppression with evidence for those permission. + NagSuppressions.addResourceSuppressionsByPath( + this, + 'AWSAccelerator-InstallerStack/GitHubPipelineRole/DefaultPolicy/Resource', + [ + { + id: 'AwsSolutions-IAM5', + reason: 'PipelineRole DefaultPolicy is built by cdk', + }, + ], + ); + + // [Error at /AWSAccelerator-InstallerStack/CodeCommitPipelineRole/DefaultPolicy/Resource] + // AwsSolutions-IAM5: The IAM entity contains wildcard permissions and does not have a cdk_nag + // rule suppression with evidence for those permission. + NagSuppressions.addResourceSuppressionsByPath( + this, + 'AWSAccelerator-InstallerStack/CodeCommitPipelineRole/DefaultPolicy/Resource', + [ + { + id: 'AwsSolutions-IAM5', + reason: 'PipelineRole DefaultPolicy is built by cdk', + }, + ], + ); + + // [Error at /AWSAccelerator-InstallerStack/CodeCommitPipeline/Source/Source/CodePipelineActionRole/DefaultPolicy/Resource] + // AwsSolutions-IAM5: The IAM entity contains wildcard permissions and does not have a cdk_nag + // rule suppression with evidence for those permission. + NagSuppressions.addResourceSuppressionsByPath( + this, + '/AWSAccelerator-InstallerStack/CodeCommitPipeline/Source/Source/CodePipelineActionRole/DefaultPolicy/Resource', + [ + { + id: 'AwsSolutions-IAM5', + reason: 'Source CodePipelineActionRole DefaultPolicy is built by cdk', + }, + ], + ); + + // [Error at /AWSAccelerator-InstallerStack/InstallerRole/Resource] + // AwsSolutions-IAM4: The IAM user, role, or group uses AWS managed policies. + NagSuppressions.addResourceSuppressionsByPath(this, '/AWSAccelerator-InstallerStack/InstallerAdminRole/Resource', [ + { + id: 'AwsSolutions-IAM4', + reason: 'Using AdministratorAccessRole to deploy accelerator pipeline', + }, + ]); + + // [Error at /AWSAccelerator-InstallerStack/InstallerRole/DefaultPolicy/Resource] + // AwsSolutions-IAM5: The IAM entity contains wildcard permissions and does not have a cdk_nag + // rule suppression with evidence for those permission. + NagSuppressions.addResourceSuppressionsByPath( + this, + '/AWSAccelerator-InstallerStack/InstallerAdminRole/DefaultPolicy/Resource', + [ + { + id: 'AwsSolutions-IAM5', + reason: 'InstallerRole DefaultPolicy is built by cdk', + }, + ], + ); + } +} diff --git a/source/packages/@aws-accelerator/installer/lib/solutions-helper.ts b/source/packages/@aws-accelerator/installer/lib/solutions-helper.ts new file mode 100644 index 000000000..3209f6ac2 --- /dev/null +++ b/source/packages/@aws-accelerator/installer/lib/solutions-helper.ts @@ -0,0 +1,189 @@ +import * as lambda from 'aws-cdk-lib/aws-lambda'; + +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import { NagSuppressions } from 'cdk-nag'; + +export interface SolutionHelperProps { + readonly solutionId: string; + readonly repositorySource: cdk.CfnParameter; + readonly repositoryOwner: cdk.CfnParameter; + readonly repositoryBranchName: cdk.CfnParameter; + readonly repositoryName: cdk.CfnParameter; +} + +export class SolutionHelper extends Construct { + constructor(scope: Construct, id: string, props: SolutionHelperProps) { + super(scope, id); + console.log(props); + + const metricsMapping = new cdk.CfnMapping(this, 'AnonymousData', { + mapping: { + SendAnonymousData: { + Data: 'Yes', + }, + }, + }); + + const metricsCondition = new cdk.CfnCondition(this, 'AnonymousDataToAWS', { + expression: cdk.Fn.conditionEquals(metricsMapping.findInMap('SendAnonymousData', 'Data'), 'Yes'), + }); + + const helperFunction = new lambda.Function(this, 'SolutionHelper', { + functionName: 'LandingZoneAccelerator-SolutionHelper', + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler', + description: + 'This function generates UUID for each deployment and sends anonymous data to the AWS Solutions team', + code: lambda.Code.fromInline(` + const AWS = require('aws-sdk'); + const response = require('cfn-response'); + const https = require('https'); + + async function post(url, data) { + const dataString = JSON.stringify(data) + const options = { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + timeout: 1000, // in ms + } + + return new Promise((resolve, reject) => { + const req = https.request(url, options, (res) => { + if (res.statusCode < 200 || res.statusCode > 299) { + return reject(new Error('HTTP status code: ', res.statusCode)) + } + const body = [] + res.on('data', (chunk) => body.push(chunk)) + res.on('end', () => { + const resString = Buffer.concat(body).toString() + resolve(resString) + }) + }) + req.on('error', (err) => { + reject(err) + }) + req.on('timeout', () => { + req.destroy() + reject(new Error('Request time out')) + }) + req.write(dataString) + req.end() + }) + } + + function uuidv4() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + } + + + function sanatizeData(resourceProperties) { + const keysToExclude = ['ServiceToken', 'Resource', 'SolutionId', 'UUID']; + return Object.keys(resourceProperties).reduce((sanatizedData, key) => { + if (!keysToExclude.includes(key)) { + sanatizedData[key] = resourceProperties[key]; + } + return sanatizedData; + }, {}) + } + + exports.handler = async function (event, context) { + console.log(JSON.stringify(event, null, 4)); + const requestType = event.RequestType; + const resourceProperties = event.ResourceProperties; + const resource = resourceProperties.Resource; + let data = {}; + try { + if (resource === 'UUID' && requestType === 'Create') { + data['UUID'] = uuidv4(); + } + if (resource === 'AnonymousMetric') { + const currentDate = new Date() + data = sanatizeData(resourceProperties); + data['RequestType'] = requestType; + const payload = { + Solution: resourceProperties.SolutionId, + UUID: resourceProperties.UUID, + TimeStamp: currentDate.toISOString(), + Data: data + } + + console.log('Sending metrics data: ', JSON.stringify(payload, null, 2)); + await post('https://metrics.awssolutionsbuilder.com/generic', payload); + console.log('Sent Data'); + } + } catch (error) { + console.log(error); + } + + await response.send(event, context, response.SUCCESS, data); + return; + } + `), + timeout: cdk.Duration.seconds(30), + }); + + NagSuppressions.addResourceSuppressions( + helperFunction, + [ + { + id: 'AwsSolutions-IAM4', + reason: 'Needed to write to CWL group', + }, + ], + true, + ); + + const cfnLambdaFunction = helperFunction.node.findChild('Resource') as lambda.CfnFunction; + cfnLambdaFunction.cfnOptions.metadata = { + cfn_nag: { + rules_to_suppress: [ + { + id: 'W58', + reason: `CloudWatch Logs are enabled in AWSLambdaBasicExecutionRole`, + }, + { + id: 'W89', + reason: `This function supports infrastructure deployment and is not deployed inside a VPC.`, + }, + { + id: 'W92', + reason: `This function supports infrastructure deployment and does not require setting ReservedConcurrentExecutions.`, + }, + ], + }, + }; + + const createIdFunction = new cdk.CustomResource(this, 'CreateUniqueID', { + serviceToken: helperFunction.functionArn, + properties: { + Resource: 'UUID', + }, + resourceType: 'Custom::CreateUUID', + }); + + const sendDataFunction = new cdk.CustomResource(this, 'SendAnonymousData', { + serviceToken: helperFunction.functionArn, + properties: { + Resource: 'AnonymousMetric', + SolutionId: props.solutionId, + UUID: createIdFunction.getAttString('UUID'), + Region: cdk.Aws.REGION, + BranchName: props.repositoryBranchName.valueAsString, + RepositoryName: props.repositoryName.valueAsString, + RepositoryOwner: props.repositoryOwner.valueAsString, + RepositorySource: props.repositorySource.valueAsString, + }, + resourceType: 'Custom::AnonymousData', + }); + + (helperFunction.node.defaultChild as lambda.CfnFunction).cfnOptions.condition = metricsCondition; + (createIdFunction.node.defaultChild as lambda.CfnFunction).cfnOptions.condition = metricsCondition; + (sendDataFunction.node.defaultChild as lambda.CfnFunction).cfnOptions.condition = metricsCondition; + } +} diff --git a/source/packages/@aws-accelerator/installer/package.json b/source/packages/@aws-accelerator/installer/package.json new file mode 100644 index 000000000..f7422ff83 --- /dev/null +++ b/source/packages/@aws-accelerator/installer/package.json @@ -0,0 +1,48 @@ +{ + "name": "@aws-accelerator/installer", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "private": true, + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "tsc", + "watch": "tsc -w", + "test": "jest --coverage --ci", + "cdk": "cdk", + "lint": "eslint --fix --max-warnings 0 -c ../../../.eslintrc.json 'lib/**/*.{ts,tsx}' 'test/**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../.eslintrc.json 'lib/**/*.{ts,tsx}' 'test/**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "aws-cdk-lib": "2.16.0", + "aws-cdk": "2.16.0", + "constructs": "10.0.12", + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "lint-staged": "12.1.2", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "ts-node": "10.4.0", + "typescript": "4.5.2" + }, + "dependencies": { + "@aws-cdk-extensions/cdk-extensions": "^0.0.0", + "@aws-accelerator/accelerator": "^0.0.0", + "aws-cdk-lib": "2.16.0", + "aws-cdk": "2.16.0", + "cdk-nag": "2.5.2" + } +} diff --git a/source/packages/@aws-accelerator/installer/test/__snapshots__/installer.test.ts.snap b/source/packages/@aws-accelerator/installer/test/__snapshots__/installer.test.ts.snap new file mode 100644 index 000000000..509eb23a5 --- /dev/null +++ b/source/packages/@aws-accelerator/installer/test/__snapshots__/installer.test.ts.snap @@ -0,0 +1,7871 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`InstallerStack Stack(installer): Snapshot test - external pipeline account stack with tester pipeline enabled 1`] = ` +Object { + "Conditions": Object { + "IsCommercialCondition": Object { + "Fn::Equals": Array [ + Object { + "Ref": "AWS::Partition", + }, + "aws", + ], + }, + "SolutionHelperAnonymousDataToAWS62E4FDE2": Object { + "Fn::Equals": Array [ + Object { + "Fn::FindInMap": Array [ + "SolutionHelperAnonymousData14B64A81", + "SendAnonymousData", + "Data", + ], + }, + "Yes", + ], + }, + "UseCodeCommitCondition": Object { + "Fn::Equals": Array [ + Object { + "Ref": "RepositorySource", + }, + "codecommit", + ], + }, + "UseGitHubCondition": Object { + "Fn::Equals": Array [ + Object { + "Ref": "RepositorySource", + }, + "github", + ], + }, + }, + "Mappings": Object { + "GlobalRegionMap": Object { + "aws": Object { + "regionName": "us-east-1", + }, + "aws-iso": Object { + "regionName": "us-iso-east-1", + }, + "aws-iso-b": Object { + "regionName": "us-isob-east-1", + }, + "aws-us-gov": Object { + "regionName": "us-gov-west-1", + }, + }, + "SolutionHelperAnonymousData14B64A81": Object { + "SendAnonymousData": Object { + "Data": "Yes", + }, + }, + }, + "Metadata": Object { + "AWS::CloudFormation::Interface": Object { + "ParameterGroups": Array [ + Object { + "Label": Object { + "default": "Git Repository Configuration", + }, + "Parameters": Array [ + "RepositorySource", + "RepositoryOwner", + "RepositoryName", + "RepositoryBranchName", + ], + }, + Object { + "Label": Object { + "default": "Pipeline Configuration", + }, + "Parameters": Array [ + "EnableApprovalStage", + "ApprovalStageNotifyEmailList", + ], + }, + Object { + "Label": Object { + "default": "Mandatory Accounts Configuration", + }, + "Parameters": Array [ + "ManagementAccountEmail", + "LogArchiveAccountEmail", + "AuditAccountEmail", + ], + }, + Object { + "Label": Object { + "default": "Target Environment Configuration", + }, + "Parameters": Array [ + "AcceleratorQualifier", + "ManagementAccountId", + "ManagementAccountRoleName", + ], + }, + ], + "ParameterLabels": Object { + "AcceleratorQualifier": Object { + "default": "Accelerator Qualifier", + }, + "ApprovalStageNotifyEmailList": Object { + "default": "Manual Approval Stage notification email list", + }, + "AuditAccountEmail": Object { + "default": "Audit Account Email", + }, + "EnableApprovalStage": Object { + "default": "Enable Approval Stage", + }, + "LogArchiveAccountEmail": Object { + "default": "Log Archive Account Email", + }, + "ManagementAccountEmail": Object { + "default": "Management Account Email", + }, + "ManagementAccountId": Object { + "default": "Management Account ID", + }, + "ManagementAccountRoleName": Object { + "default": "Management Account Role Name", + }, + "RepositoryBranchName": Object { + "default": "Branch Name", + }, + "RepositoryName": Object { + "default": "Repository Name", + }, + "RepositoryOwner": Object { + "default": "Repository Owner", + }, + "RepositorySource": Object { + "default": "Source", + }, + }, + }, + }, + "Parameters": Object { + "AcceleratorQualifier": Object { + "AllowedPattern": "^[a-z]+[a-z0-9-]{1,61}[a-z0-9]+$", + "Description": "Accelerator assets arn qualifier", + "Type": "String", + }, + "ApprovalStageNotifyEmailList": Object { + "Description": "Provide comma(,) separated list of email ids to receive manual approval stage notification email", + "Type": "CommaDelimitedList", + }, + "AuditAccountEmail": Object { + "Description": "The security audit account (also referred to as the audit account)", + "Type": "String", + }, + "EnableApprovalStage": Object { + "AllowedValues": Array [ + "Yes", + "No", + ], + "Default": "Yes", + "Description": "Select yes to add a Manual Approval stage to accelerator pipeline", + "Type": "String", + }, + "LogArchiveAccountEmail": Object { + "Description": "The log archive account email", + "Type": "String", + }, + "ManagementAccountEmail": Object { + "Description": "The management (primary) account email", + "Type": "String", + }, + "ManagementAccountId": Object { + "Description": "Target management account id", + "Type": "String", + }, + "ManagementAccountRoleName": Object { + "Description": "Target management account role name", + "Type": "String", + }, + "RepositoryBranchName": Object { + "Description": "The name of the git branch to use for installation", + "Type": "String", + }, + "RepositoryName": Object { + "Default": "landing-zone-accelerator-on-aws", + "Description": "The name of the git repository hosting the accelerator code", + "Type": "String", + }, + "RepositoryOwner": Object { + "Default": "awslabs", + "Description": "The owner of the repository containing the accelerator code. (GitHub Only)", + "Type": "String", + }, + "RepositorySource": Object { + "AllowedValues": Array [ + "github", + "codecommit", + ], + "Default": "github", + "Description": "Specify the git host", + "Type": "String", + }, + }, + "Resources": Object { + "AcceleratorManagementKmsArnParameter1E6975BF": Object { + "Properties": Object { + "Name": Object { + "Fn::Join": Array [ + "", + Array [ + "/accelerator/", + Object { + "Ref": "AcceleratorQualifier", + }, + "/installer/kms/key-arn", + ], + ], + }, + "Type": "String", + "Value": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + }, + "Type": "AWS::SSM::Parameter", + }, + "CodeCommitPipeline2208527B": Object { + "Condition": "UseCodeCommitCondition", + "DependsOn": Array [ + "CodeCommitPipelineRoleDefaultPolicyDE8B332B", + "CodeCommitPipelineRole5C35E76C", + ], + "Properties": Object { + "ArtifactStore": Object { + "EncryptionKey": Object { + "Id": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + "Type": "KMS", + }, + "Location": Object { + "Ref": "SecureBucket747CD8C0", + }, + "Type": "S3", + }, + "Name": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Ref": "AcceleratorQualifier", + }, + "-installer", + ], + ], + }, + "RestartExecutionOnUpdate": true, + "RoleArn": Object { + "Fn::GetAtt": Array [ + "CodeCommitPipelineRole5C35E76C", + "Arn", + ], + }, + "Stages": Array [ + Object { + "Actions": Array [ + Object { + "ActionTypeId": Object { + "Category": "Source", + "Owner": "AWS", + "Provider": "CodeCommit", + "Version": "1", + }, + "Configuration": Object { + "BranchName": Object { + "Ref": "RepositoryBranchName", + }, + "PollForSourceChanges": false, + "RepositoryName": Object { + "Ref": "RepositoryName", + }, + }, + "Name": "Source", + "OutputArtifacts": Array [ + Object { + "Name": "Source", + }, + ], + "RoleArn": Object { + "Fn::GetAtt": Array [ + "CodeCommitPipelineSourceCodePipelineActionRoleFB176191", + "Arn", + ], + }, + "RunOrder": 1, + }, + ], + "Name": "Source", + }, + Object { + "Actions": Array [ + Object { + "ActionTypeId": Object { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1", + }, + "Configuration": Object { + "ProjectName": Object { + "Ref": "InstallerProject879FF821", + }, + }, + "InputArtifacts": Array [ + Object { + "Name": "Source", + }, + ], + "Name": "Install", + "RoleArn": Object { + "Fn::GetAtt": Array [ + "CodeCommitPipelineRole5C35E76C", + "Arn", + ], + }, + "RunOrder": 1, + }, + ], + "Name": "Install", + }, + ], + }, + "Type": "AWS::CodePipeline::Pipeline", + }, + "CodeCommitPipelineRole5C35E76C": Object { + "Condition": "UseCodeCommitCondition", + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "codepipeline.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::IAM::Role", + }, + "CodeCommitPipelineRoleDefaultPolicyDE8B332B": Object { + "Condition": "UseCodeCommitCondition", + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", + "s3:Abort*", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "SecureBucket747CD8C0", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "SecureBucket747CD8C0", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + Object { + "Action": Array [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + }, + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "CodeCommitPipelineSourceCodePipelineActionRoleFB176191", + "Arn", + ], + }, + }, + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "CodeCommitPipelineRole5C35E76C", + "Arn", + ], + }, + }, + Object { + "Action": Array [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "InstallerProject879FF821", + "Arn", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "CodeCommitPipelineRoleDefaultPolicyDE8B332B", + "Roles": Array [ + Object { + "Ref": "CodeCommitPipelineRole5C35E76C", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "CodeCommitPipelineSourceCodePipelineActionRoleDefaultPolicyF71E0C0D": Object { + "Condition": "UseCodeCommitCondition", + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", + "s3:Abort*", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "SecureBucket747CD8C0", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "SecureBucket747CD8C0", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + Object { + "Action": Array [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + }, + Object { + "Action": Array [ + "codecommit:GetBranch", + "codecommit:GetCommit", + "codecommit:UploadArchive", + "codecommit:GetUploadArchiveStatus", + "codecommit:CancelUploadArchive", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":codecommit:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "RepositoryName", + }, + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "CodeCommitPipelineSourceCodePipelineActionRoleDefaultPolicyF71E0C0D", + "Roles": Array [ + Object { + "Ref": "CodeCommitPipelineSourceCodePipelineActionRoleFB176191", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "CodeCommitPipelineSourceCodePipelineActionRoleFB176191": Object { + "Condition": "UseCodeCommitCondition", + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::", + Object { + "Ref": "AWS::AccountId", + }, + ":root", + ], + ], + }, + }, + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::IAM::Role", + }, + "GitHubPipeline7B79E906": Object { + "Condition": "UseGitHubCondition", + "DependsOn": Array [ + "GitHubPipelineRoleDefaultPolicyD82457D6", + "GitHubPipelineRole6F4DEF1B", + ], + "Properties": Object { + "ArtifactStore": Object { + "EncryptionKey": Object { + "Id": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + "Type": "KMS", + }, + "Location": Object { + "Ref": "SecureBucket747CD8C0", + }, + "Type": "S3", + }, + "Name": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Ref": "AcceleratorQualifier", + }, + "-installer", + ], + ], + }, + "RestartExecutionOnUpdate": true, + "RoleArn": Object { + "Fn::GetAtt": Array [ + "GitHubPipelineRole6F4DEF1B", + "Arn", + ], + }, + "Stages": Array [ + Object { + "Actions": Array [ + Object { + "ActionTypeId": Object { + "Category": "Source", + "Owner": "ThirdParty", + "Provider": "GitHub", + "Version": "1", + }, + "Configuration": Object { + "Branch": Object { + "Ref": "RepositoryBranchName", + }, + "OAuthToken": "{{resolve:secretsmanager:accelerator/github-token:SecretString:::}}", + "Owner": Object { + "Ref": "RepositoryOwner", + }, + "PollForSourceChanges": false, + "Repo": Object { + "Ref": "RepositoryName", + }, + }, + "Name": "Source", + "OutputArtifacts": Array [ + Object { + "Name": "Source", + }, + ], + "RunOrder": 1, + }, + ], + "Name": "Source", + }, + Object { + "Actions": Array [ + Object { + "ActionTypeId": Object { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1", + }, + "Configuration": Object { + "ProjectName": Object { + "Ref": "InstallerProject879FF821", + }, + }, + "InputArtifacts": Array [ + Object { + "Name": "Source", + }, + ], + "Name": "Install", + "RoleArn": Object { + "Fn::GetAtt": Array [ + "GitHubPipelineRole6F4DEF1B", + "Arn", + ], + }, + "RunOrder": 1, + }, + ], + "Name": "Install", + }, + ], + }, + "Type": "AWS::CodePipeline::Pipeline", + }, + "GitHubPipelineRole6F4DEF1B": Object { + "Condition": "UseGitHubCondition", + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "codepipeline.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::IAM::Role", + }, + "GitHubPipelineRoleDefaultPolicyD82457D6": Object { + "Condition": "UseGitHubCondition", + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", + "s3:Abort*", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "SecureBucket747CD8C0", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "SecureBucket747CD8C0", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + Object { + "Action": Array [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + }, + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "GitHubPipelineRole6F4DEF1B", + "Arn", + ], + }, + }, + Object { + "Action": Array [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "InstallerProject879FF821", + "Arn", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "GitHubPipelineRoleDefaultPolicyD82457D6", + "Roles": Array [ + Object { + "Ref": "GitHubPipelineRole6F4DEF1B", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "InstallerAccessLogsBucket647700E9": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cdk_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "AwsSolutions-S1", + "reason": "AccessLogsBucket has server access logs disabled till the task for access logging completed.", + }, + ], + }, + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W35", + "reason": "This is an access logging bucket.", + }, + ], + }, + }, + "Properties": Object { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "BucketName": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Ref": "AcceleratorQualifier", + }, + "-s3-logs-", + Object { + "Ref": "AWS::AccountId", + }, + "-", + Object { + "Ref": "AWS::Region", + }, + ], + ], + }, + "LifecycleConfiguration": Object { + "Rules": Array [ + Object { + "AbortIncompleteMultipartUpload": Object { + "DaysAfterInitiation": 1, + }, + "ExpirationInDays": 1825, + "ExpiredObjectDeleteMarker": false, + "Id": Object { + "Fn::Join": Array [ + "", + Array [ + "LifecycleRule", + Object { + "Ref": "AcceleratorQualifier", + }, + "-s3-logs-", + Object { + "Ref": "AWS::AccountId", + }, + "-", + Object { + "Ref": "AWS::Region", + }, + ], + ], + }, + "NoncurrentVersionExpirationInDays": 1825, + "NoncurrentVersionTransitions": Array [ + Object { + "StorageClass": "DEEP_ARCHIVE", + "TransitionInDays": 366, + }, + ], + "Status": "Enabled", + "Transitions": Array [ + Object { + "StorageClass": "DEEP_ARCHIVE", + "TransitionInDays": 365, + }, + ], + }, + ], + }, + "OwnershipControls": Object { + "Rules": Array [ + Object { + "ObjectOwnership": "BucketOwnerPreferred", + }, + ], + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "InstallerAccessLogsBucketName4F700F48": Object { + "Properties": Object { + "Name": Object { + "Fn::Join": Array [ + "", + Array [ + "/accelerator/", + Object { + "Ref": "AcceleratorQualifier", + }, + "/installer-access-logs-bucket-name", + ], + ], + }, + "Type": "String", + "Value": Object { + "Ref": "InstallerAccessLogsBucket647700E9", + }, + }, + "Type": "AWS::SSM::Parameter", + }, + "InstallerAccessLogsBucketPolicy20D4E285": Object { + "Properties": Object { + "Bucket": Object { + "Ref": "InstallerAccessLogsBucket647700E9", + }, + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "s3:*", + "Condition": Object { + "Bool": Object { + "aws:SecureTransport": "false", + }, + }, + "Effect": "Deny", + "Principal": Object { + "AWS": "*", + }, + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "InstallerAccessLogsBucket647700E9", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "InstallerAccessLogsBucket647700E9", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + "Sid": "deny-insecure-connections", + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::S3::BucketPolicy", + }, + "InstallerAdminRole7DEE4AC8": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "codebuild.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/AdministratorAccess", + ], + ], + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "InstallerAdminRoleDefaultPolicy7EEE1AAB": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/codebuild/", + Object { + "Ref": "InstallerProject879FF821", + }, + ], + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/codebuild/", + Object { + "Ref": "InstallerProject879FF821", + }, + ":*", + ], + ], + }, + ], + }, + Object { + "Action": Array [ + "codebuild:CreateReportGroup", + "codebuild:CreateReport", + "codebuild:UpdateReport", + "codebuild:BatchPutTestCases", + "codebuild:BatchPutCodeCoverages", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":codebuild:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":report-group/", + Object { + "Ref": "InstallerProject879FF821", + }, + "-*", + ], + ], + }, + }, + Object { + "Action": Array [ + "kms:Decrypt", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + }, + Object { + "Action": Array [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "SecureBucket747CD8C0", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "SecureBucket747CD8C0", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + Object { + "Action": Array [ + "kms:Decrypt", + "kms:DescribeKey", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "InstallerAdminRoleDefaultPolicy7EEE1AAB", + "Roles": Array [ + Object { + "Ref": "InstallerAdminRole7DEE4AC8", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "InstallerKey2A6A8C6D": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "F76", + "reason": "KMS key using * principal with added arn condition", + }, + ], + }, + }, + "Properties": Object { + "Description": "AWS Accelerator Management Account Kms Key", + "EnableKeyRotation": true, + "KeyPolicy": Object { + "Statement": Array [ + Object { + "Action": "kms:*", + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::", + Object { + "Ref": "AWS::AccountId", + }, + ":root", + ], + ], + }, + }, + "Resource": "*", + }, + Object { + "Action": Array [ + "kms:Encrypt", + "kms:Decrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:DescribeKey", + ], + "Condition": Object { + "ArnLike": Object { + "aws:PrincipalARN": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::", + Object { + "Ref": "AWS::AccountId", + }, + ":role/", + Object { + "Ref": "AcceleratorQualifier", + }, + "-*", + ], + ], + }, + }, + }, + "Effect": "Allow", + "Principal": Object { + "AWS": "*", + }, + "Resource": "*", + "Sid": "Allow Accelerator Role to use the encryption key", + }, + Object { + "Action": Array [ + "kms:Encrypt", + "kms:Decrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:DescribeKey", + ], + "Effect": "Allow", + "Principal": Object { + "Service": "sns.amazonaws.com", + }, + "Resource": "*", + "Sid": "Allow SNS service to use the encryption key", + }, + Object { + "Fn::If": Array [ + "IsCommercialCondition", + Object { + "Action": Array [ + "kms:GenerateDataKey*", + "kms:Decrypt", + ], + "Condition": Object { + "StringEquals": Object { + "kms:ViaService": Object { + "Fn::Join": Array [ + "", + Array [ + "sns.", + Object { + "Ref": "AWS::Region", + }, + ".amazonaws.com", + ], + ], + }, + }, + }, + "Effect": "Allow", + "Principal": Object { + "Service": "codestar-notifications.amazonaws.com", + }, + "Resource": "*", + "Sid": "KMS key access to codestar-notifications", + }, + Object { + "Ref": "AWS::NoValue", + }, + ], + }, + ], + }, + }, + "Type": "AWS::KMS::Key", + "UpdateReplacePolicy": "Retain", + }, + "InstallerKeyAliasD5C174F0": Object { + "Properties": Object { + "AliasName": Object { + "Fn::Join": Array [ + "", + Array [ + "alias/accelerator/", + Object { + "Ref": "AcceleratorQualifier", + }, + "/installer/kms/key", + ], + ], + }, + "TargetKeyId": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + }, + "Type": "AWS::KMS::Alias", + }, + "InstallerProject879FF821": Object { + "Properties": Object { + "Artifacts": Object { + "Type": "CODEPIPELINE", + }, + "Cache": Object { + "Type": "NO_CACHE", + }, + "EncryptionKey": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + "Environment": Object { + "ComputeType": "BUILD_GENERAL1_MEDIUM", + "EnvironmentVariables": Array [ + Object { + "Name": "NODE_OPTIONS", + "Type": "PLAINTEXT", + "Value": "--max_old_space_size=4096", + }, + Object { + "Name": "CDK_NEW_BOOTSTRAP", + "Type": "PLAINTEXT", + "Value": "1", + }, + Object { + "Name": "ACCELERATOR_REPOSITORY_SOURCE", + "Type": "PLAINTEXT", + "Value": Object { + "Ref": "RepositorySource", + }, + }, + Object { + "Name": "ACCELERATOR_REPOSITORY_OWNER", + "Type": "PLAINTEXT", + "Value": Object { + "Ref": "RepositoryOwner", + }, + }, + Object { + "Name": "ACCELERATOR_REPOSITORY_NAME", + "Type": "PLAINTEXT", + "Value": Object { + "Ref": "RepositoryName", + }, + }, + Object { + "Name": "ACCELERATOR_REPOSITORY_BRANCH_NAME", + "Type": "PLAINTEXT", + "Value": Object { + "Ref": "RepositoryBranchName", + }, + }, + Object { + "Name": "ACCELERATOR_ENABLE_APPROVAL_STAGE", + "Type": "PLAINTEXT", + "Value": Object { + "Ref": "EnableApprovalStage", + }, + }, + Object { + "Name": "APPROVAL_STAGE_NOTIFY_EMAIL_LIST", + "Type": "PLAINTEXT", + "Value": Object { + "Fn::Join": Array [ + ",", + Object { + "Ref": "ApprovalStageNotifyEmailList", + }, + ], + }, + }, + Object { + "Name": "MANAGEMENT_ACCOUNT_EMAIL", + "Type": "PLAINTEXT", + "Value": Object { + "Ref": "ManagementAccountEmail", + }, + }, + Object { + "Name": "LOG_ARCHIVE_ACCOUNT_EMAIL", + "Type": "PLAINTEXT", + "Value": Object { + "Ref": "LogArchiveAccountEmail", + }, + }, + Object { + "Name": "AUDIT_ACCOUNT_EMAIL", + "Type": "PLAINTEXT", + "Value": Object { + "Ref": "AuditAccountEmail", + }, + }, + Object { + "Name": "MANAGEMENT_ACCOUNT_ID", + "Type": "PLAINTEXT", + "Value": Object { + "Ref": "ManagementAccountId", + }, + }, + Object { + "Name": "MANAGEMENT_ACCOUNT_ROLE_NAME", + "Type": "PLAINTEXT", + "Value": Object { + "Ref": "ManagementAccountRoleName", + }, + }, + Object { + "Name": "ACCELERATOR_QUALIFIER", + "Type": "PLAINTEXT", + "Value": Object { + "Ref": "AcceleratorQualifier", + }, + }, + Object { + "Name": "ENABLE_TESTER", + "Type": "PLAINTEXT", + "Value": "true", + }, + Object { + "Name": "MANAGEMENT_CROSS_ACCOUNT_ROLE_NAME", + "Type": "PLAINTEXT", + "Value": "AWSControlTowerExecution", + }, + ], + "Image": "aws/codebuild/standard:5.0", + "ImagePullCredentialsType": "CODEBUILD", + "PrivilegedMode": true, + "Type": "LINUX_CONTAINER", + }, + "Name": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Ref": "AcceleratorQualifier", + }, + "-installer-project", + ], + ], + }, + "ServiceRole": Object { + "Fn::GetAtt": Array [ + "InstallerAdminRole7DEE4AC8", + "Arn", + ], + }, + "Source": Object { + "BuildSpec": Object { + "Fn::Join": Array [ + "", + Array [ + "version: \\"0.2\\" +phases: + install: + runtime-versions: + nodejs: 14 + pre_build: + commands: + - ENABLE_EXTERNAL_PIPELINE_ACCOUNT=\\"no\\" + - if [ ! -z \\"$MANAGEMENT_ACCOUNT_ID\\" ] && [ ! -z \\"$MANAGEMENT_ACCOUNT_ROLE_NAME\\" ]; then ENABLE_EXTERNAL_PIPELINE_ACCOUNT=\\"yes\\"; fi + build: + commands: + - cd source + - yarn install + - yarn lerna link + - yarn build + - cd packages/@aws-accelerator/installer + - yarn run cdk bootstrap --toolkitStackName AWSAccelerator-CDKToolkit aws://", + Object { + "Ref": "AWS::AccountId", + }, + "/", + Object { + "Ref": "AWS::Region", + }, + " --qualifier accel + - yarn run cdk bootstrap --toolkitStackName AWSAccelerator-CDKToolkit aws://", + Object { + "Ref": "AWS::AccountId", + }, + "/", + Object { + "Fn::FindInMap": Array [ + "GlobalRegionMap", + Object { + "Ref": "AWS::Partition", + }, + "regionName", + ], + }, + " --qualifier accel + - |- + if [ $ENABLE_EXTERNAL_PIPELINE_ACCOUNT = \\"yes\\" ]; then + export $(printf \\"AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s\\" $(aws sts assume-role --role-arn arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::\\"$MANAGEMENT_ACCOUNT_ID\\":role/\\"$MANAGEMENT_ACCOUNT_ROLE_NAME\\" --role-session-name acceleratorAssumeRoleSession --query \\"Credentials.[AccessKeyId,SecretAccessKey,SessionToken]\\" --output text)); + yarn run cdk bootstrap --toolkitStackName AWSAccelerator-CDKToolkit aws://$MANAGEMENT_ACCOUNT_ID/", + Object { + "Ref": "AWS::Region", + }, + " --qualifier accel; + yarn run cdk bootstrap --toolkitStackName AWSAccelerator-CDKToolkit aws://$MANAGEMENT_ACCOUNT_ID/", + Object { + "Fn::FindInMap": Array [ + "GlobalRegionMap", + Object { + "Ref": "AWS::Partition", + }, + "regionName", + ], + }, + " --qualifier accel; + unset AWS_ACCESS_KEY_ID; + unset AWS_SECRET_ACCESS_KEY; + unset AWS_SESSION_TOKEN; + fi + - cd ../accelerator + - yarn run ts-node --transpile-only cdk.ts deploy --require-approval never --stage pipeline --account ", + Object { + "Ref": "AWS::AccountId", + }, + " --region ", + Object { + "Ref": "AWS::Region", + }, + " --partition ", + Object { + "Ref": "AWS::Partition", + }, + " + - if [ \\"$ENABLE_TESTER\\" = \\"true\\" ]; then yarn run ts-node --transpile-only cdk.ts deploy --require-approval never --stage tester-pipeline --account ", + Object { + "Ref": "AWS::AccountId", + }, + " --region ", + Object { + "Ref": "AWS::Region", + }, + "; fi +", + ], + ], + }, + "Type": "CODEPIPELINE", + }, + }, + "Type": "AWS::CodeBuild::Project", + }, + "SecureBucket747CD8C0": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "KMSMasterKeyID": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + "SSEAlgorithm": "aws:kms", + }, + }, + ], + }, + "BucketName": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Ref": "AcceleratorQualifier", + }, + "-installer-", + Object { + "Ref": "AWS::AccountId", + }, + "-", + Object { + "Ref": "AWS::Region", + }, + ], + ], + }, + "LifecycleConfiguration": Object { + "Rules": Array [ + Object { + "AbortIncompleteMultipartUpload": Object { + "DaysAfterInitiation": 1, + }, + "ExpirationInDays": 1825, + "ExpiredObjectDeleteMarker": false, + "Id": Object { + "Fn::Join": Array [ + "", + Array [ + "LifecycleRule", + Object { + "Ref": "AcceleratorQualifier", + }, + "-installer-", + Object { + "Ref": "AWS::AccountId", + }, + "-", + Object { + "Ref": "AWS::Region", + }, + ], + ], + }, + "NoncurrentVersionExpirationInDays": 1825, + "NoncurrentVersionTransitions": Array [ + Object { + "StorageClass": "DEEP_ARCHIVE", + "TransitionInDays": 366, + }, + ], + "Status": "Enabled", + "Transitions": Array [ + Object { + "StorageClass": "DEEP_ARCHIVE", + "TransitionInDays": 365, + }, + ], + }, + ], + }, + "LoggingConfiguration": Object { + "DestinationBucketName": Object { + "Ref": "InstallerAccessLogsBucket647700E9", + }, + "LogFilePrefix": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Ref": "AcceleratorQualifier", + }, + "-installer-", + Object { + "Ref": "AWS::AccountId", + }, + "-", + Object { + "Ref": "AWS::Region", + }, + "/", + ], + ], + }, + }, + "OwnershipControls": Object { + "Rules": Array [ + Object { + "ObjectOwnership": "BucketOwnerPreferred", + }, + ], + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "SecureBucketPolicy6374AC61": Object { + "Properties": Object { + "Bucket": Object { + "Ref": "SecureBucket747CD8C0", + }, + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "s3:*", + "Condition": Object { + "Bool": Object { + "aws:SecureTransport": "false", + }, + }, + "Effect": "Deny", + "Principal": Object { + "AWS": "*", + }, + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "SecureBucket747CD8C0", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "SecureBucket747CD8C0", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + "Sid": "deny-insecure-connections", + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::S3::BucketPolicy", + }, + "SolutionHelper4825923B": Object { + "Condition": "SolutionHelperAnonymousDataToAWS62E4FDE2", + "DependsOn": Array [ + "SolutionHelperServiceRoleF70C0E2A", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W58", + "reason": "CloudWatch Logs are enabled in AWSLambdaBasicExecutionRole", + }, + Object { + "id": "W89", + "reason": "This function supports infrastructure deployment and is not deployed inside a VPC.", + }, + Object { + "id": "W92", + "reason": "This function supports infrastructure deployment and does not require setting ReservedConcurrentExecutions.", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "ZipFile": " + const AWS = require('aws-sdk'); + const response = require('cfn-response'); + const https = require('https'); + + async function post(url, data) { + const dataString = JSON.stringify(data) + const options = { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + timeout: 1000, // in ms + } + + return new Promise((resolve, reject) => { + const req = https.request(url, options, (res) => { + if (res.statusCode < 200 || res.statusCode > 299) { + return reject(new Error('HTTP status code: ', res.statusCode)) + } + const body = [] + res.on('data', (chunk) => body.push(chunk)) + res.on('end', () => { + const resString = Buffer.concat(body).toString() + resolve(resString) + }) + }) + req.on('error', (err) => { + reject(err) + }) + req.on('timeout', () => { + req.destroy() + reject(new Error('Request time out')) + }) + req.write(dataString) + req.end() + }) + } + + function uuidv4() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + } + + + function sanatizeData(resourceProperties) { + const keysToExclude = ['ServiceToken', 'Resource', 'SolutionId', 'UUID']; + return Object.keys(resourceProperties).reduce((sanatizedData, key) => { + if (!keysToExclude.includes(key)) { + sanatizedData[key] = resourceProperties[key]; + } + return sanatizedData; + }, {}) + } + + exports.handler = async function (event, context) { + console.log(JSON.stringify(event, null, 4)); + const requestType = event.RequestType; + const resourceProperties = event.ResourceProperties; + const resource = resourceProperties.Resource; + let data = {}; + try { + if (resource === 'UUID' && requestType === 'Create') { + data['UUID'] = uuidv4(); + } + if (resource === 'AnonymousMetric') { + const currentDate = new Date() + data = sanatizeData(resourceProperties); + data['RequestType'] = requestType; + const payload = { + Solution: resourceProperties.SolutionId, + UUID: resourceProperties.UUID, + TimeStamp: currentDate.toISOString(), + Data: data + } + + console.log('Sending metrics data: ', JSON.stringify(payload, null, 2)); + await post('https://metrics.awssolutionsbuilder.com/generic', payload); + console.log('Sent Data'); + } + } catch (error) { + console.log(error); + } + + await response.send(event, context, response.SUCCESS, data); + return; + } + ", + }, + "Description": "This function generates UUID for each deployment and sends anonymous data to the AWS Solutions team", + "FunctionName": "LandingZoneAccelerator-SolutionHelper", + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "SolutionHelperServiceRoleF70C0E2A", + "Arn", + ], + }, + "Runtime": "nodejs14.x", + "Timeout": 30, + }, + "Type": "AWS::Lambda::Function", + }, + "SolutionHelperCreateUniqueIDAF03406A": Object { + "Condition": "SolutionHelperAnonymousDataToAWS62E4FDE2", + "DeletionPolicy": "Delete", + "Properties": Object { + "Resource": "UUID", + "ServiceToken": Object { + "Fn::GetAtt": Array [ + "SolutionHelper4825923B", + "Arn", + ], + }, + }, + "Type": "Custom::CreateUUID", + "UpdateReplacePolicy": "Delete", + }, + "SolutionHelperSendAnonymousData9B19E31D": Object { + "Condition": "SolutionHelperAnonymousDataToAWS62E4FDE2", + "DeletionPolicy": "Delete", + "Properties": Object { + "BranchName": Object { + "Ref": "RepositoryBranchName", + }, + "Region": Object { + "Ref": "AWS::Region", + }, + "RepositoryName": Object { + "Ref": "RepositoryName", + }, + "RepositoryOwner": Object { + "Ref": "RepositoryOwner", + }, + "RepositorySource": Object { + "Ref": "RepositorySource", + }, + "Resource": "AnonymousMetric", + "ServiceToken": Object { + "Fn::GetAtt": Array [ + "SolutionHelper4825923B", + "Arn", + ], + }, + "SolutionId": "SO0199", + "UUID": Object { + "Fn::GetAtt": Array [ + "SolutionHelperCreateUniqueIDAF03406A", + "UUID", + ], + }, + }, + "Type": "Custom::AnonymousData", + "UpdateReplacePolicy": "Delete", + }, + "SolutionHelperServiceRoleF70C0E2A": Object { + "Metadata": Object { + "cdk_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "AwsSolutions-IAM4", + "reason": "Needed to write to CWL group", + }, + ], + }, + }, + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ], + ], + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "SsmParamAcceleratorVersionFF83282D": Object { + "Properties": Object { + "Name": Object { + "Fn::Join": Array [ + "", + Array [ + "/accelerator/", + Object { + "Ref": "AcceleratorQualifier", + }, + "/AWSAccelerator-Test-InstallerStack/version", + ], + ], + }, + "Type": "String", + "Value": "1.0.0", + }, + "Type": "AWS::SSM::Parameter", + }, + "SsmParamStackId521A78D3": Object { + "Properties": Object { + "Name": Object { + "Fn::Join": Array [ + "", + Array [ + "/accelerator/", + Object { + "Ref": "AcceleratorQualifier", + }, + "/AWSAccelerator-Test-InstallerStack/stack-id", + ], + ], + }, + "Type": "String", + "Value": Object { + "Ref": "AWS::StackId", + }, + }, + "Type": "AWS::SSM::Parameter", + }, + }, +} +`; + +exports[`InstallerStack Stack(installer): Snapshot test - external pipeline account stack without tester pipeline enabled 1`] = ` +Object { + "Conditions": Object { + "IsCommercialCondition": Object { + "Fn::Equals": Array [ + Object { + "Ref": "AWS::Partition", + }, + "aws", + ], + }, + "SolutionHelperAnonymousDataToAWS62E4FDE2": Object { + "Fn::Equals": Array [ + Object { + "Fn::FindInMap": Array [ + "SolutionHelperAnonymousData14B64A81", + "SendAnonymousData", + "Data", + ], + }, + "Yes", + ], + }, + "UseCodeCommitCondition": Object { + "Fn::Equals": Array [ + Object { + "Ref": "RepositorySource", + }, + "codecommit", + ], + }, + "UseGitHubCondition": Object { + "Fn::Equals": Array [ + Object { + "Ref": "RepositorySource", + }, + "github", + ], + }, + }, + "Mappings": Object { + "GlobalRegionMap": Object { + "aws": Object { + "regionName": "us-east-1", + }, + "aws-iso": Object { + "regionName": "us-iso-east-1", + }, + "aws-iso-b": Object { + "regionName": "us-isob-east-1", + }, + "aws-us-gov": Object { + "regionName": "us-gov-west-1", + }, + }, + "SolutionHelperAnonymousData14B64A81": Object { + "SendAnonymousData": Object { + "Data": "Yes", + }, + }, + }, + "Metadata": Object { + "AWS::CloudFormation::Interface": Object { + "ParameterGroups": Array [ + Object { + "Label": Object { + "default": "Git Repository Configuration", + }, + "Parameters": Array [ + "RepositorySource", + "RepositoryOwner", + "RepositoryName", + "RepositoryBranchName", + ], + }, + Object { + "Label": Object { + "default": "Pipeline Configuration", + }, + "Parameters": Array [ + "EnableApprovalStage", + "ApprovalStageNotifyEmailList", + ], + }, + Object { + "Label": Object { + "default": "Mandatory Accounts Configuration", + }, + "Parameters": Array [ + "ManagementAccountEmail", + "LogArchiveAccountEmail", + "AuditAccountEmail", + ], + }, + Object { + "Label": Object { + "default": "Target Environment Configuration", + }, + "Parameters": Array [ + "AcceleratorQualifier", + "ManagementAccountId", + "ManagementAccountRoleName", + ], + }, + ], + "ParameterLabels": Object { + "AcceleratorQualifier": Object { + "default": "Accelerator Qualifier", + }, + "ApprovalStageNotifyEmailList": Object { + "default": "Manual Approval Stage notification email list", + }, + "AuditAccountEmail": Object { + "default": "Audit Account Email", + }, + "EnableApprovalStage": Object { + "default": "Enable Approval Stage", + }, + "LogArchiveAccountEmail": Object { + "default": "Log Archive Account Email", + }, + "ManagementAccountEmail": Object { + "default": "Management Account Email", + }, + "ManagementAccountId": Object { + "default": "Management Account ID", + }, + "ManagementAccountRoleName": Object { + "default": "Management Account Role Name", + }, + "RepositoryBranchName": Object { + "default": "Branch Name", + }, + "RepositoryName": Object { + "default": "Repository Name", + }, + "RepositoryOwner": Object { + "default": "Repository Owner", + }, + "RepositorySource": Object { + "default": "Source", + }, + }, + }, + }, + "Parameters": Object { + "AcceleratorQualifier": Object { + "AllowedPattern": "^[a-z]+[a-z0-9-]{1,61}[a-z0-9]+$", + "Description": "Accelerator assets arn qualifier", + "Type": "String", + }, + "ApprovalStageNotifyEmailList": Object { + "Description": "Provide comma(,) separated list of email ids to receive manual approval stage notification email", + "Type": "CommaDelimitedList", + }, + "AuditAccountEmail": Object { + "Description": "The security audit account (also referred to as the audit account)", + "Type": "String", + }, + "EnableApprovalStage": Object { + "AllowedValues": Array [ + "Yes", + "No", + ], + "Default": "Yes", + "Description": "Select yes to add a Manual Approval stage to accelerator pipeline", + "Type": "String", + }, + "LogArchiveAccountEmail": Object { + "Description": "The log archive account email", + "Type": "String", + }, + "ManagementAccountEmail": Object { + "Description": "The management (primary) account email", + "Type": "String", + }, + "ManagementAccountId": Object { + "Description": "Target management account id", + "Type": "String", + }, + "ManagementAccountRoleName": Object { + "Description": "Target management account role name", + "Type": "String", + }, + "RepositoryBranchName": Object { + "Description": "The name of the git branch to use for installation", + "Type": "String", + }, + "RepositoryName": Object { + "Default": "landing-zone-accelerator-on-aws", + "Description": "The name of the git repository hosting the accelerator code", + "Type": "String", + }, + "RepositoryOwner": Object { + "Default": "awslabs", + "Description": "The owner of the repository containing the accelerator code. (GitHub Only)", + "Type": "String", + }, + "RepositorySource": Object { + "AllowedValues": Array [ + "github", + "codecommit", + ], + "Default": "github", + "Description": "Specify the git host", + "Type": "String", + }, + }, + "Resources": Object { + "AcceleratorManagementKmsArnParameter1E6975BF": Object { + "Properties": Object { + "Name": Object { + "Fn::Join": Array [ + "", + Array [ + "/accelerator/", + Object { + "Ref": "AcceleratorQualifier", + }, + "/installer/kms/key-arn", + ], + ], + }, + "Type": "String", + "Value": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + }, + "Type": "AWS::SSM::Parameter", + }, + "CodeCommitPipeline2208527B": Object { + "Condition": "UseCodeCommitCondition", + "DependsOn": Array [ + "CodeCommitPipelineRoleDefaultPolicyDE8B332B", + "CodeCommitPipelineRole5C35E76C", + ], + "Properties": Object { + "ArtifactStore": Object { + "EncryptionKey": Object { + "Id": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + "Type": "KMS", + }, + "Location": Object { + "Ref": "SecureBucket747CD8C0", + }, + "Type": "S3", + }, + "Name": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Ref": "AcceleratorQualifier", + }, + "-installer", + ], + ], + }, + "RestartExecutionOnUpdate": true, + "RoleArn": Object { + "Fn::GetAtt": Array [ + "CodeCommitPipelineRole5C35E76C", + "Arn", + ], + }, + "Stages": Array [ + Object { + "Actions": Array [ + Object { + "ActionTypeId": Object { + "Category": "Source", + "Owner": "AWS", + "Provider": "CodeCommit", + "Version": "1", + }, + "Configuration": Object { + "BranchName": Object { + "Ref": "RepositoryBranchName", + }, + "PollForSourceChanges": false, + "RepositoryName": Object { + "Ref": "RepositoryName", + }, + }, + "Name": "Source", + "OutputArtifacts": Array [ + Object { + "Name": "Source", + }, + ], + "RoleArn": Object { + "Fn::GetAtt": Array [ + "CodeCommitPipelineSourceCodePipelineActionRoleFB176191", + "Arn", + ], + }, + "RunOrder": 1, + }, + ], + "Name": "Source", + }, + Object { + "Actions": Array [ + Object { + "ActionTypeId": Object { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1", + }, + "Configuration": Object { + "ProjectName": Object { + "Ref": "InstallerProject879FF821", + }, + }, + "InputArtifacts": Array [ + Object { + "Name": "Source", + }, + ], + "Name": "Install", + "RoleArn": Object { + "Fn::GetAtt": Array [ + "CodeCommitPipelineRole5C35E76C", + "Arn", + ], + }, + "RunOrder": 1, + }, + ], + "Name": "Install", + }, + ], + }, + "Type": "AWS::CodePipeline::Pipeline", + }, + "CodeCommitPipelineRole5C35E76C": Object { + "Condition": "UseCodeCommitCondition", + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "codepipeline.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::IAM::Role", + }, + "CodeCommitPipelineRoleDefaultPolicyDE8B332B": Object { + "Condition": "UseCodeCommitCondition", + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", + "s3:Abort*", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "SecureBucket747CD8C0", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "SecureBucket747CD8C0", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + Object { + "Action": Array [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + }, + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "CodeCommitPipelineSourceCodePipelineActionRoleFB176191", + "Arn", + ], + }, + }, + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "CodeCommitPipelineRole5C35E76C", + "Arn", + ], + }, + }, + Object { + "Action": Array [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "InstallerProject879FF821", + "Arn", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "CodeCommitPipelineRoleDefaultPolicyDE8B332B", + "Roles": Array [ + Object { + "Ref": "CodeCommitPipelineRole5C35E76C", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "CodeCommitPipelineSourceCodePipelineActionRoleDefaultPolicyF71E0C0D": Object { + "Condition": "UseCodeCommitCondition", + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", + "s3:Abort*", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "SecureBucket747CD8C0", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "SecureBucket747CD8C0", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + Object { + "Action": Array [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + }, + Object { + "Action": Array [ + "codecommit:GetBranch", + "codecommit:GetCommit", + "codecommit:UploadArchive", + "codecommit:GetUploadArchiveStatus", + "codecommit:CancelUploadArchive", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":codecommit:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "RepositoryName", + }, + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "CodeCommitPipelineSourceCodePipelineActionRoleDefaultPolicyF71E0C0D", + "Roles": Array [ + Object { + "Ref": "CodeCommitPipelineSourceCodePipelineActionRoleFB176191", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "CodeCommitPipelineSourceCodePipelineActionRoleFB176191": Object { + "Condition": "UseCodeCommitCondition", + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::", + Object { + "Ref": "AWS::AccountId", + }, + ":root", + ], + ], + }, + }, + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::IAM::Role", + }, + "GitHubPipeline7B79E906": Object { + "Condition": "UseGitHubCondition", + "DependsOn": Array [ + "GitHubPipelineRoleDefaultPolicyD82457D6", + "GitHubPipelineRole6F4DEF1B", + ], + "Properties": Object { + "ArtifactStore": Object { + "EncryptionKey": Object { + "Id": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + "Type": "KMS", + }, + "Location": Object { + "Ref": "SecureBucket747CD8C0", + }, + "Type": "S3", + }, + "Name": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Ref": "AcceleratorQualifier", + }, + "-installer", + ], + ], + }, + "RestartExecutionOnUpdate": true, + "RoleArn": Object { + "Fn::GetAtt": Array [ + "GitHubPipelineRole6F4DEF1B", + "Arn", + ], + }, + "Stages": Array [ + Object { + "Actions": Array [ + Object { + "ActionTypeId": Object { + "Category": "Source", + "Owner": "ThirdParty", + "Provider": "GitHub", + "Version": "1", + }, + "Configuration": Object { + "Branch": Object { + "Ref": "RepositoryBranchName", + }, + "OAuthToken": "{{resolve:secretsmanager:accelerator/github-token:SecretString:::}}", + "Owner": Object { + "Ref": "RepositoryOwner", + }, + "PollForSourceChanges": false, + "Repo": Object { + "Ref": "RepositoryName", + }, + }, + "Name": "Source", + "OutputArtifacts": Array [ + Object { + "Name": "Source", + }, + ], + "RunOrder": 1, + }, + ], + "Name": "Source", + }, + Object { + "Actions": Array [ + Object { + "ActionTypeId": Object { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1", + }, + "Configuration": Object { + "ProjectName": Object { + "Ref": "InstallerProject879FF821", + }, + }, + "InputArtifacts": Array [ + Object { + "Name": "Source", + }, + ], + "Name": "Install", + "RoleArn": Object { + "Fn::GetAtt": Array [ + "GitHubPipelineRole6F4DEF1B", + "Arn", + ], + }, + "RunOrder": 1, + }, + ], + "Name": "Install", + }, + ], + }, + "Type": "AWS::CodePipeline::Pipeline", + }, + "GitHubPipelineRole6F4DEF1B": Object { + "Condition": "UseGitHubCondition", + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "codepipeline.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::IAM::Role", + }, + "GitHubPipelineRoleDefaultPolicyD82457D6": Object { + "Condition": "UseGitHubCondition", + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", + "s3:Abort*", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "SecureBucket747CD8C0", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "SecureBucket747CD8C0", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + Object { + "Action": Array [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + }, + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "GitHubPipelineRole6F4DEF1B", + "Arn", + ], + }, + }, + Object { + "Action": Array [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "InstallerProject879FF821", + "Arn", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "GitHubPipelineRoleDefaultPolicyD82457D6", + "Roles": Array [ + Object { + "Ref": "GitHubPipelineRole6F4DEF1B", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "InstallerAccessLogsBucket647700E9": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cdk_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "AwsSolutions-S1", + "reason": "AccessLogsBucket has server access logs disabled till the task for access logging completed.", + }, + ], + }, + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W35", + "reason": "This is an access logging bucket.", + }, + ], + }, + }, + "Properties": Object { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "BucketName": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Ref": "AcceleratorQualifier", + }, + "-s3-logs-", + Object { + "Ref": "AWS::AccountId", + }, + "-", + Object { + "Ref": "AWS::Region", + }, + ], + ], + }, + "LifecycleConfiguration": Object { + "Rules": Array [ + Object { + "AbortIncompleteMultipartUpload": Object { + "DaysAfterInitiation": 1, + }, + "ExpirationInDays": 1825, + "ExpiredObjectDeleteMarker": false, + "Id": Object { + "Fn::Join": Array [ + "", + Array [ + "LifecycleRule", + Object { + "Ref": "AcceleratorQualifier", + }, + "-s3-logs-", + Object { + "Ref": "AWS::AccountId", + }, + "-", + Object { + "Ref": "AWS::Region", + }, + ], + ], + }, + "NoncurrentVersionExpirationInDays": 1825, + "NoncurrentVersionTransitions": Array [ + Object { + "StorageClass": "DEEP_ARCHIVE", + "TransitionInDays": 366, + }, + ], + "Status": "Enabled", + "Transitions": Array [ + Object { + "StorageClass": "DEEP_ARCHIVE", + "TransitionInDays": 365, + }, + ], + }, + ], + }, + "OwnershipControls": Object { + "Rules": Array [ + Object { + "ObjectOwnership": "BucketOwnerPreferred", + }, + ], + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "InstallerAccessLogsBucketName4F700F48": Object { + "Properties": Object { + "Name": Object { + "Fn::Join": Array [ + "", + Array [ + "/accelerator/", + Object { + "Ref": "AcceleratorQualifier", + }, + "/installer-access-logs-bucket-name", + ], + ], + }, + "Type": "String", + "Value": Object { + "Ref": "InstallerAccessLogsBucket647700E9", + }, + }, + "Type": "AWS::SSM::Parameter", + }, + "InstallerAccessLogsBucketPolicy20D4E285": Object { + "Properties": Object { + "Bucket": Object { + "Ref": "InstallerAccessLogsBucket647700E9", + }, + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "s3:*", + "Condition": Object { + "Bool": Object { + "aws:SecureTransport": "false", + }, + }, + "Effect": "Deny", + "Principal": Object { + "AWS": "*", + }, + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "InstallerAccessLogsBucket647700E9", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "InstallerAccessLogsBucket647700E9", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + "Sid": "deny-insecure-connections", + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::S3::BucketPolicy", + }, + "InstallerAdminRole7DEE4AC8": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "codebuild.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/AdministratorAccess", + ], + ], + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "InstallerAdminRoleDefaultPolicy7EEE1AAB": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/codebuild/", + Object { + "Ref": "InstallerProject879FF821", + }, + ], + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/codebuild/", + Object { + "Ref": "InstallerProject879FF821", + }, + ":*", + ], + ], + }, + ], + }, + Object { + "Action": Array [ + "codebuild:CreateReportGroup", + "codebuild:CreateReport", + "codebuild:UpdateReport", + "codebuild:BatchPutTestCases", + "codebuild:BatchPutCodeCoverages", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":codebuild:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":report-group/", + Object { + "Ref": "InstallerProject879FF821", + }, + "-*", + ], + ], + }, + }, + Object { + "Action": Array [ + "kms:Decrypt", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + }, + Object { + "Action": Array [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "SecureBucket747CD8C0", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "SecureBucket747CD8C0", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + Object { + "Action": Array [ + "kms:Decrypt", + "kms:DescribeKey", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "InstallerAdminRoleDefaultPolicy7EEE1AAB", + "Roles": Array [ + Object { + "Ref": "InstallerAdminRole7DEE4AC8", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "InstallerKey2A6A8C6D": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "F76", + "reason": "KMS key using * principal with added arn condition", + }, + ], + }, + }, + "Properties": Object { + "Description": "AWS Accelerator Management Account Kms Key", + "EnableKeyRotation": true, + "KeyPolicy": Object { + "Statement": Array [ + Object { + "Action": "kms:*", + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::", + Object { + "Ref": "AWS::AccountId", + }, + ":root", + ], + ], + }, + }, + "Resource": "*", + }, + Object { + "Action": Array [ + "kms:Encrypt", + "kms:Decrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:DescribeKey", + ], + "Condition": Object { + "ArnLike": Object { + "aws:PrincipalARN": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::", + Object { + "Ref": "AWS::AccountId", + }, + ":role/", + Object { + "Ref": "AcceleratorQualifier", + }, + "-*", + ], + ], + }, + }, + }, + "Effect": "Allow", + "Principal": Object { + "AWS": "*", + }, + "Resource": "*", + "Sid": "Allow Accelerator Role to use the encryption key", + }, + Object { + "Action": Array [ + "kms:Encrypt", + "kms:Decrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:DescribeKey", + ], + "Effect": "Allow", + "Principal": Object { + "Service": "sns.amazonaws.com", + }, + "Resource": "*", + "Sid": "Allow SNS service to use the encryption key", + }, + Object { + "Fn::If": Array [ + "IsCommercialCondition", + Object { + "Action": Array [ + "kms:GenerateDataKey*", + "kms:Decrypt", + ], + "Condition": Object { + "StringEquals": Object { + "kms:ViaService": Object { + "Fn::Join": Array [ + "", + Array [ + "sns.", + Object { + "Ref": "AWS::Region", + }, + ".amazonaws.com", + ], + ], + }, + }, + }, + "Effect": "Allow", + "Principal": Object { + "Service": "codestar-notifications.amazonaws.com", + }, + "Resource": "*", + "Sid": "KMS key access to codestar-notifications", + }, + Object { + "Ref": "AWS::NoValue", + }, + ], + }, + ], + }, + }, + "Type": "AWS::KMS::Key", + "UpdateReplacePolicy": "Retain", + }, + "InstallerKeyAliasD5C174F0": Object { + "Properties": Object { + "AliasName": Object { + "Fn::Join": Array [ + "", + Array [ + "alias/accelerator/", + Object { + "Ref": "AcceleratorQualifier", + }, + "/installer/kms/key", + ], + ], + }, + "TargetKeyId": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + }, + "Type": "AWS::KMS::Alias", + }, + "InstallerProject879FF821": Object { + "Properties": Object { + "Artifacts": Object { + "Type": "CODEPIPELINE", + }, + "Cache": Object { + "Type": "NO_CACHE", + }, + "EncryptionKey": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + "Environment": Object { + "ComputeType": "BUILD_GENERAL1_MEDIUM", + "EnvironmentVariables": Array [ + Object { + "Name": "NODE_OPTIONS", + "Type": "PLAINTEXT", + "Value": "--max_old_space_size=4096", + }, + Object { + "Name": "CDK_NEW_BOOTSTRAP", + "Type": "PLAINTEXT", + "Value": "1", + }, + Object { + "Name": "ACCELERATOR_REPOSITORY_SOURCE", + "Type": "PLAINTEXT", + "Value": Object { + "Ref": "RepositorySource", + }, + }, + Object { + "Name": "ACCELERATOR_REPOSITORY_OWNER", + "Type": "PLAINTEXT", + "Value": Object { + "Ref": "RepositoryOwner", + }, + }, + Object { + "Name": "ACCELERATOR_REPOSITORY_NAME", + "Type": "PLAINTEXT", + "Value": Object { + "Ref": "RepositoryName", + }, + }, + Object { + "Name": "ACCELERATOR_REPOSITORY_BRANCH_NAME", + "Type": "PLAINTEXT", + "Value": Object { + "Ref": "RepositoryBranchName", + }, + }, + Object { + "Name": "ACCELERATOR_ENABLE_APPROVAL_STAGE", + "Type": "PLAINTEXT", + "Value": Object { + "Ref": "EnableApprovalStage", + }, + }, + Object { + "Name": "APPROVAL_STAGE_NOTIFY_EMAIL_LIST", + "Type": "PLAINTEXT", + "Value": Object { + "Fn::Join": Array [ + ",", + Object { + "Ref": "ApprovalStageNotifyEmailList", + }, + ], + }, + }, + Object { + "Name": "MANAGEMENT_ACCOUNT_EMAIL", + "Type": "PLAINTEXT", + "Value": Object { + "Ref": "ManagementAccountEmail", + }, + }, + Object { + "Name": "LOG_ARCHIVE_ACCOUNT_EMAIL", + "Type": "PLAINTEXT", + "Value": Object { + "Ref": "LogArchiveAccountEmail", + }, + }, + Object { + "Name": "AUDIT_ACCOUNT_EMAIL", + "Type": "PLAINTEXT", + "Value": Object { + "Ref": "AuditAccountEmail", + }, + }, + Object { + "Name": "MANAGEMENT_ACCOUNT_ID", + "Type": "PLAINTEXT", + "Value": Object { + "Ref": "ManagementAccountId", + }, + }, + Object { + "Name": "MANAGEMENT_ACCOUNT_ROLE_NAME", + "Type": "PLAINTEXT", + "Value": Object { + "Ref": "ManagementAccountRoleName", + }, + }, + Object { + "Name": "ACCELERATOR_QUALIFIER", + "Type": "PLAINTEXT", + "Value": Object { + "Ref": "AcceleratorQualifier", + }, + }, + ], + "Image": "aws/codebuild/standard:5.0", + "ImagePullCredentialsType": "CODEBUILD", + "PrivilegedMode": true, + "Type": "LINUX_CONTAINER", + }, + "Name": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Ref": "AcceleratorQualifier", + }, + "-installer-project", + ], + ], + }, + "ServiceRole": Object { + "Fn::GetAtt": Array [ + "InstallerAdminRole7DEE4AC8", + "Arn", + ], + }, + "Source": Object { + "BuildSpec": Object { + "Fn::Join": Array [ + "", + Array [ + "version: \\"0.2\\" +phases: + install: + runtime-versions: + nodejs: 14 + pre_build: + commands: + - ENABLE_EXTERNAL_PIPELINE_ACCOUNT=\\"no\\" + - if [ ! -z \\"$MANAGEMENT_ACCOUNT_ID\\" ] && [ ! -z \\"$MANAGEMENT_ACCOUNT_ROLE_NAME\\" ]; then ENABLE_EXTERNAL_PIPELINE_ACCOUNT=\\"yes\\"; fi + build: + commands: + - cd source + - yarn install + - yarn lerna link + - yarn build + - cd packages/@aws-accelerator/installer + - yarn run cdk bootstrap --toolkitStackName AWSAccelerator-CDKToolkit aws://", + Object { + "Ref": "AWS::AccountId", + }, + "/", + Object { + "Ref": "AWS::Region", + }, + " --qualifier accel + - yarn run cdk bootstrap --toolkitStackName AWSAccelerator-CDKToolkit aws://", + Object { + "Ref": "AWS::AccountId", + }, + "/", + Object { + "Fn::FindInMap": Array [ + "GlobalRegionMap", + Object { + "Ref": "AWS::Partition", + }, + "regionName", + ], + }, + " --qualifier accel + - |- + if [ $ENABLE_EXTERNAL_PIPELINE_ACCOUNT = \\"yes\\" ]; then + export $(printf \\"AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s\\" $(aws sts assume-role --role-arn arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::\\"$MANAGEMENT_ACCOUNT_ID\\":role/\\"$MANAGEMENT_ACCOUNT_ROLE_NAME\\" --role-session-name acceleratorAssumeRoleSession --query \\"Credentials.[AccessKeyId,SecretAccessKey,SessionToken]\\" --output text)); + yarn run cdk bootstrap --toolkitStackName AWSAccelerator-CDKToolkit aws://$MANAGEMENT_ACCOUNT_ID/", + Object { + "Ref": "AWS::Region", + }, + " --qualifier accel; + yarn run cdk bootstrap --toolkitStackName AWSAccelerator-CDKToolkit aws://$MANAGEMENT_ACCOUNT_ID/", + Object { + "Fn::FindInMap": Array [ + "GlobalRegionMap", + Object { + "Ref": "AWS::Partition", + }, + "regionName", + ], + }, + " --qualifier accel; + unset AWS_ACCESS_KEY_ID; + unset AWS_SECRET_ACCESS_KEY; + unset AWS_SESSION_TOKEN; + fi + - cd ../accelerator + - yarn run ts-node --transpile-only cdk.ts deploy --require-approval never --stage pipeline --account ", + Object { + "Ref": "AWS::AccountId", + }, + " --region ", + Object { + "Ref": "AWS::Region", + }, + " --partition ", + Object { + "Ref": "AWS::Partition", + }, + " + - if [ \\"$ENABLE_TESTER\\" = \\"true\\" ]; then yarn run ts-node --transpile-only cdk.ts deploy --require-approval never --stage tester-pipeline --account ", + Object { + "Ref": "AWS::AccountId", + }, + " --region ", + Object { + "Ref": "AWS::Region", + }, + "; fi +", + ], + ], + }, + "Type": "CODEPIPELINE", + }, + }, + "Type": "AWS::CodeBuild::Project", + }, + "SecureBucket747CD8C0": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "KMSMasterKeyID": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + "SSEAlgorithm": "aws:kms", + }, + }, + ], + }, + "BucketName": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Ref": "AcceleratorQualifier", + }, + "-installer-", + Object { + "Ref": "AWS::AccountId", + }, + "-", + Object { + "Ref": "AWS::Region", + }, + ], + ], + }, + "LifecycleConfiguration": Object { + "Rules": Array [ + Object { + "AbortIncompleteMultipartUpload": Object { + "DaysAfterInitiation": 1, + }, + "ExpirationInDays": 1825, + "ExpiredObjectDeleteMarker": false, + "Id": Object { + "Fn::Join": Array [ + "", + Array [ + "LifecycleRule", + Object { + "Ref": "AcceleratorQualifier", + }, + "-installer-", + Object { + "Ref": "AWS::AccountId", + }, + "-", + Object { + "Ref": "AWS::Region", + }, + ], + ], + }, + "NoncurrentVersionExpirationInDays": 1825, + "NoncurrentVersionTransitions": Array [ + Object { + "StorageClass": "DEEP_ARCHIVE", + "TransitionInDays": 366, + }, + ], + "Status": "Enabled", + "Transitions": Array [ + Object { + "StorageClass": "DEEP_ARCHIVE", + "TransitionInDays": 365, + }, + ], + }, + ], + }, + "LoggingConfiguration": Object { + "DestinationBucketName": Object { + "Ref": "InstallerAccessLogsBucket647700E9", + }, + "LogFilePrefix": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Ref": "AcceleratorQualifier", + }, + "-installer-", + Object { + "Ref": "AWS::AccountId", + }, + "-", + Object { + "Ref": "AWS::Region", + }, + "/", + ], + ], + }, + }, + "OwnershipControls": Object { + "Rules": Array [ + Object { + "ObjectOwnership": "BucketOwnerPreferred", + }, + ], + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "SecureBucketPolicy6374AC61": Object { + "Properties": Object { + "Bucket": Object { + "Ref": "SecureBucket747CD8C0", + }, + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "s3:*", + "Condition": Object { + "Bool": Object { + "aws:SecureTransport": "false", + }, + }, + "Effect": "Deny", + "Principal": Object { + "AWS": "*", + }, + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "SecureBucket747CD8C0", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "SecureBucket747CD8C0", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + "Sid": "deny-insecure-connections", + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::S3::BucketPolicy", + }, + "SolutionHelper4825923B": Object { + "Condition": "SolutionHelperAnonymousDataToAWS62E4FDE2", + "DependsOn": Array [ + "SolutionHelperServiceRoleF70C0E2A", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W58", + "reason": "CloudWatch Logs are enabled in AWSLambdaBasicExecutionRole", + }, + Object { + "id": "W89", + "reason": "This function supports infrastructure deployment and is not deployed inside a VPC.", + }, + Object { + "id": "W92", + "reason": "This function supports infrastructure deployment and does not require setting ReservedConcurrentExecutions.", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "ZipFile": " + const AWS = require('aws-sdk'); + const response = require('cfn-response'); + const https = require('https'); + + async function post(url, data) { + const dataString = JSON.stringify(data) + const options = { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + timeout: 1000, // in ms + } + + return new Promise((resolve, reject) => { + const req = https.request(url, options, (res) => { + if (res.statusCode < 200 || res.statusCode > 299) { + return reject(new Error('HTTP status code: ', res.statusCode)) + } + const body = [] + res.on('data', (chunk) => body.push(chunk)) + res.on('end', () => { + const resString = Buffer.concat(body).toString() + resolve(resString) + }) + }) + req.on('error', (err) => { + reject(err) + }) + req.on('timeout', () => { + req.destroy() + reject(new Error('Request time out')) + }) + req.write(dataString) + req.end() + }) + } + + function uuidv4() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + } + + + function sanatizeData(resourceProperties) { + const keysToExclude = ['ServiceToken', 'Resource', 'SolutionId', 'UUID']; + return Object.keys(resourceProperties).reduce((sanatizedData, key) => { + if (!keysToExclude.includes(key)) { + sanatizedData[key] = resourceProperties[key]; + } + return sanatizedData; + }, {}) + } + + exports.handler = async function (event, context) { + console.log(JSON.stringify(event, null, 4)); + const requestType = event.RequestType; + const resourceProperties = event.ResourceProperties; + const resource = resourceProperties.Resource; + let data = {}; + try { + if (resource === 'UUID' && requestType === 'Create') { + data['UUID'] = uuidv4(); + } + if (resource === 'AnonymousMetric') { + const currentDate = new Date() + data = sanatizeData(resourceProperties); + data['RequestType'] = requestType; + const payload = { + Solution: resourceProperties.SolutionId, + UUID: resourceProperties.UUID, + TimeStamp: currentDate.toISOString(), + Data: data + } + + console.log('Sending metrics data: ', JSON.stringify(payload, null, 2)); + await post('https://metrics.awssolutionsbuilder.com/generic', payload); + console.log('Sent Data'); + } + } catch (error) { + console.log(error); + } + + await response.send(event, context, response.SUCCESS, data); + return; + } + ", + }, + "Description": "This function generates UUID for each deployment and sends anonymous data to the AWS Solutions team", + "FunctionName": "LandingZoneAccelerator-SolutionHelper", + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "SolutionHelperServiceRoleF70C0E2A", + "Arn", + ], + }, + "Runtime": "nodejs14.x", + "Timeout": 30, + }, + "Type": "AWS::Lambda::Function", + }, + "SolutionHelperCreateUniqueIDAF03406A": Object { + "Condition": "SolutionHelperAnonymousDataToAWS62E4FDE2", + "DeletionPolicy": "Delete", + "Properties": Object { + "Resource": "UUID", + "ServiceToken": Object { + "Fn::GetAtt": Array [ + "SolutionHelper4825923B", + "Arn", + ], + }, + }, + "Type": "Custom::CreateUUID", + "UpdateReplacePolicy": "Delete", + }, + "SolutionHelperSendAnonymousData9B19E31D": Object { + "Condition": "SolutionHelperAnonymousDataToAWS62E4FDE2", + "DeletionPolicy": "Delete", + "Properties": Object { + "BranchName": Object { + "Ref": "RepositoryBranchName", + }, + "Region": Object { + "Ref": "AWS::Region", + }, + "RepositoryName": Object { + "Ref": "RepositoryName", + }, + "RepositoryOwner": Object { + "Ref": "RepositoryOwner", + }, + "RepositorySource": Object { + "Ref": "RepositorySource", + }, + "Resource": "AnonymousMetric", + "ServiceToken": Object { + "Fn::GetAtt": Array [ + "SolutionHelper4825923B", + "Arn", + ], + }, + "SolutionId": "SO0199", + "UUID": Object { + "Fn::GetAtt": Array [ + "SolutionHelperCreateUniqueIDAF03406A", + "UUID", + ], + }, + }, + "Type": "Custom::AnonymousData", + "UpdateReplacePolicy": "Delete", + }, + "SolutionHelperServiceRoleF70C0E2A": Object { + "Metadata": Object { + "cdk_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "AwsSolutions-IAM4", + "reason": "Needed to write to CWL group", + }, + ], + }, + }, + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ], + ], + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "SsmParamAcceleratorVersionFF83282D": Object { + "Properties": Object { + "Name": Object { + "Fn::Join": Array [ + "", + Array [ + "/accelerator/", + Object { + "Ref": "AcceleratorQualifier", + }, + "/AWSAccelerator-Test-InstallerStack/version", + ], + ], + }, + "Type": "String", + "Value": "1.0.0", + }, + "Type": "AWS::SSM::Parameter", + }, + "SsmParamStackId521A78D3": Object { + "Properties": Object { + "Name": Object { + "Fn::Join": Array [ + "", + Array [ + "/accelerator/", + Object { + "Ref": "AcceleratorQualifier", + }, + "/AWSAccelerator-Test-InstallerStack/stack-id", + ], + ], + }, + "Type": "String", + "Value": Object { + "Ref": "AWS::StackId", + }, + }, + "Type": "AWS::SSM::Parameter", + }, + }, +} +`; + +exports[`InstallerStack Stack(installer): Snapshot test - management account stack with tester pipeline enabled 1`] = ` +Object { + "Conditions": Object { + "IsCommercialCondition": Object { + "Fn::Equals": Array [ + Object { + "Ref": "AWS::Partition", + }, + "aws", + ], + }, + "SolutionHelperAnonymousDataToAWS62E4FDE2": Object { + "Fn::Equals": Array [ + Object { + "Fn::FindInMap": Array [ + "SolutionHelperAnonymousData14B64A81", + "SendAnonymousData", + "Data", + ], + }, + "Yes", + ], + }, + "UseCodeCommitCondition": Object { + "Fn::Equals": Array [ + Object { + "Ref": "RepositorySource", + }, + "codecommit", + ], + }, + "UseGitHubCondition": Object { + "Fn::Equals": Array [ + Object { + "Ref": "RepositorySource", + }, + "github", + ], + }, + }, + "Mappings": Object { + "GlobalRegionMap": Object { + "aws": Object { + "regionName": "us-east-1", + }, + "aws-iso": Object { + "regionName": "us-iso-east-1", + }, + "aws-iso-b": Object { + "regionName": "us-isob-east-1", + }, + "aws-us-gov": Object { + "regionName": "us-gov-west-1", + }, + }, + "SolutionHelperAnonymousData14B64A81": Object { + "SendAnonymousData": Object { + "Data": "Yes", + }, + }, + }, + "Metadata": Object { + "AWS::CloudFormation::Interface": Object { + "ParameterGroups": Array [ + Object { + "Label": Object { + "default": "Git Repository Configuration", + }, + "Parameters": Array [ + "RepositorySource", + "RepositoryOwner", + "RepositoryName", + "RepositoryBranchName", + ], + }, + Object { + "Label": Object { + "default": "Pipeline Configuration", + }, + "Parameters": Array [ + "EnableApprovalStage", + "ApprovalStageNotifyEmailList", + ], + }, + Object { + "Label": Object { + "default": "Mandatory Accounts Configuration", + }, + "Parameters": Array [ + "ManagementAccountEmail", + "LogArchiveAccountEmail", + "AuditAccountEmail", + ], + }, + ], + "ParameterLabels": Object { + "ApprovalStageNotifyEmailList": Object { + "default": "Manual Approval Stage notification email list", + }, + "AuditAccountEmail": Object { + "default": "Audit Account Email", + }, + "EnableApprovalStage": Object { + "default": "Enable Approval Stage", + }, + "LogArchiveAccountEmail": Object { + "default": "Log Archive Account Email", + }, + "ManagementAccountEmail": Object { + "default": "Management Account Email", + }, + "RepositoryBranchName": Object { + "default": "Branch Name", + }, + "RepositoryName": Object { + "default": "Repository Name", + }, + "RepositoryOwner": Object { + "default": "Repository Owner", + }, + "RepositorySource": Object { + "default": "Source", + }, + }, + }, + }, + "Parameters": Object { + "ApprovalStageNotifyEmailList": Object { + "Description": "Provide comma(,) separated list of email ids to receive manual approval stage notification email", + "Type": "CommaDelimitedList", + }, + "AuditAccountEmail": Object { + "Description": "The security audit account (also referred to as the audit account)", + "Type": "String", + }, + "EnableApprovalStage": Object { + "AllowedValues": Array [ + "Yes", + "No", + ], + "Default": "Yes", + "Description": "Select yes to add a Manual Approval stage to accelerator pipeline", + "Type": "String", + }, + "LogArchiveAccountEmail": Object { + "Description": "The log archive account email", + "Type": "String", + }, + "ManagementAccountEmail": Object { + "Description": "The management (primary) account email", + "Type": "String", + }, + "RepositoryBranchName": Object { + "Description": "The name of the git branch to use for installation", + "Type": "String", + }, + "RepositoryName": Object { + "Default": "landing-zone-accelerator-on-aws", + "Description": "The name of the git repository hosting the accelerator code", + "Type": "String", + }, + "RepositoryOwner": Object { + "Default": "awslabs", + "Description": "The owner of the repository containing the accelerator code. (GitHub Only)", + "Type": "String", + }, + "RepositorySource": Object { + "AllowedValues": Array [ + "github", + "codecommit", + ], + "Default": "github", + "Description": "Specify the git host", + "Type": "String", + }, + }, + "Resources": Object { + "AcceleratorManagementKmsArnParameter1E6975BF": Object { + "Properties": Object { + "Name": "/accelerator/installer/kms/key-arn", + "Type": "String", + "Value": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + }, + "Type": "AWS::SSM::Parameter", + }, + "CodeCommitPipeline2208527B": Object { + "Condition": "UseCodeCommitCondition", + "DependsOn": Array [ + "CodeCommitPipelineRoleDefaultPolicyDE8B332B", + "CodeCommitPipelineRole5C35E76C", + ], + "Properties": Object { + "ArtifactStore": Object { + "EncryptionKey": Object { + "Id": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + "Type": "KMS", + }, + "Location": Object { + "Ref": "SecureBucket747CD8C0", + }, + "Type": "S3", + }, + "Name": "AWSAccelerator-Installer", + "RestartExecutionOnUpdate": true, + "RoleArn": Object { + "Fn::GetAtt": Array [ + "CodeCommitPipelineRole5C35E76C", + "Arn", + ], + }, + "Stages": Array [ + Object { + "Actions": Array [ + Object { + "ActionTypeId": Object { + "Category": "Source", + "Owner": "AWS", + "Provider": "CodeCommit", + "Version": "1", + }, + "Configuration": Object { + "BranchName": Object { + "Ref": "RepositoryBranchName", + }, + "PollForSourceChanges": false, + "RepositoryName": Object { + "Ref": "RepositoryName", + }, + }, + "Name": "Source", + "OutputArtifacts": Array [ + Object { + "Name": "Source", + }, + ], + "RoleArn": Object { + "Fn::GetAtt": Array [ + "CodeCommitPipelineSourceCodePipelineActionRoleFB176191", + "Arn", + ], + }, + "RunOrder": 1, + }, + ], + "Name": "Source", + }, + Object { + "Actions": Array [ + Object { + "ActionTypeId": Object { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1", + }, + "Configuration": Object { + "ProjectName": Object { + "Ref": "InstallerProject879FF821", + }, + }, + "InputArtifacts": Array [ + Object { + "Name": "Source", + }, + ], + "Name": "Install", + "RoleArn": Object { + "Fn::GetAtt": Array [ + "CodeCommitPipelineRole5C35E76C", + "Arn", + ], + }, + "RunOrder": 1, + }, + ], + "Name": "Install", + }, + ], + }, + "Type": "AWS::CodePipeline::Pipeline", + }, + "CodeCommitPipelineRole5C35E76C": Object { + "Condition": "UseCodeCommitCondition", + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "codepipeline.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::IAM::Role", + }, + "CodeCommitPipelineRoleDefaultPolicyDE8B332B": Object { + "Condition": "UseCodeCommitCondition", + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", + "s3:Abort*", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "SecureBucket747CD8C0", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "SecureBucket747CD8C0", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + Object { + "Action": Array [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + }, + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "CodeCommitPipelineSourceCodePipelineActionRoleFB176191", + "Arn", + ], + }, + }, + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "CodeCommitPipelineRole5C35E76C", + "Arn", + ], + }, + }, + Object { + "Action": Array [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "InstallerProject879FF821", + "Arn", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "CodeCommitPipelineRoleDefaultPolicyDE8B332B", + "Roles": Array [ + Object { + "Ref": "CodeCommitPipelineRole5C35E76C", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "CodeCommitPipelineSourceCodePipelineActionRoleDefaultPolicyF71E0C0D": Object { + "Condition": "UseCodeCommitCondition", + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", + "s3:Abort*", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "SecureBucket747CD8C0", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "SecureBucket747CD8C0", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + Object { + "Action": Array [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + }, + Object { + "Action": Array [ + "codecommit:GetBranch", + "codecommit:GetCommit", + "codecommit:UploadArchive", + "codecommit:GetUploadArchiveStatus", + "codecommit:CancelUploadArchive", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":codecommit:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "RepositoryName", + }, + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "CodeCommitPipelineSourceCodePipelineActionRoleDefaultPolicyF71E0C0D", + "Roles": Array [ + Object { + "Ref": "CodeCommitPipelineSourceCodePipelineActionRoleFB176191", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "CodeCommitPipelineSourceCodePipelineActionRoleFB176191": Object { + "Condition": "UseCodeCommitCondition", + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::", + Object { + "Ref": "AWS::AccountId", + }, + ":root", + ], + ], + }, + }, + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::IAM::Role", + }, + "GitHubPipeline7B79E906": Object { + "Condition": "UseGitHubCondition", + "DependsOn": Array [ + "GitHubPipelineRoleDefaultPolicyD82457D6", + "GitHubPipelineRole6F4DEF1B", + ], + "Properties": Object { + "ArtifactStore": Object { + "EncryptionKey": Object { + "Id": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + "Type": "KMS", + }, + "Location": Object { + "Ref": "SecureBucket747CD8C0", + }, + "Type": "S3", + }, + "Name": "AWSAccelerator-Installer", + "RestartExecutionOnUpdate": true, + "RoleArn": Object { + "Fn::GetAtt": Array [ + "GitHubPipelineRole6F4DEF1B", + "Arn", + ], + }, + "Stages": Array [ + Object { + "Actions": Array [ + Object { + "ActionTypeId": Object { + "Category": "Source", + "Owner": "ThirdParty", + "Provider": "GitHub", + "Version": "1", + }, + "Configuration": Object { + "Branch": Object { + "Ref": "RepositoryBranchName", + }, + "OAuthToken": "{{resolve:secretsmanager:accelerator/github-token:SecretString:::}}", + "Owner": Object { + "Ref": "RepositoryOwner", + }, + "PollForSourceChanges": false, + "Repo": Object { + "Ref": "RepositoryName", + }, + }, + "Name": "Source", + "OutputArtifacts": Array [ + Object { + "Name": "Source", + }, + ], + "RunOrder": 1, + }, + ], + "Name": "Source", + }, + Object { + "Actions": Array [ + Object { + "ActionTypeId": Object { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1", + }, + "Configuration": Object { + "ProjectName": Object { + "Ref": "InstallerProject879FF821", + }, + }, + "InputArtifacts": Array [ + Object { + "Name": "Source", + }, + ], + "Name": "Install", + "RoleArn": Object { + "Fn::GetAtt": Array [ + "GitHubPipelineRole6F4DEF1B", + "Arn", + ], + }, + "RunOrder": 1, + }, + ], + "Name": "Install", + }, + ], + }, + "Type": "AWS::CodePipeline::Pipeline", + }, + "GitHubPipelineRole6F4DEF1B": Object { + "Condition": "UseGitHubCondition", + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "codepipeline.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::IAM::Role", + }, + "GitHubPipelineRoleDefaultPolicyD82457D6": Object { + "Condition": "UseGitHubCondition", + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", + "s3:Abort*", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "SecureBucket747CD8C0", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "SecureBucket747CD8C0", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + Object { + "Action": Array [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + }, + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "GitHubPipelineRole6F4DEF1B", + "Arn", + ], + }, + }, + Object { + "Action": Array [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "InstallerProject879FF821", + "Arn", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "GitHubPipelineRoleDefaultPolicyD82457D6", + "Roles": Array [ + Object { + "Ref": "GitHubPipelineRole6F4DEF1B", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "InstallerAccessLogsBucket647700E9": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cdk_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "AwsSolutions-S1", + "reason": "AccessLogsBucket has server access logs disabled till the task for access logging completed.", + }, + ], + }, + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W35", + "reason": "This is an access logging bucket.", + }, + ], + }, + }, + "Properties": Object { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "BucketName": Object { + "Fn::Join": Array [ + "", + Array [ + "aws-accelerator-s3-logs-", + Object { + "Ref": "AWS::AccountId", + }, + "-", + Object { + "Ref": "AWS::Region", + }, + ], + ], + }, + "LifecycleConfiguration": Object { + "Rules": Array [ + Object { + "AbortIncompleteMultipartUpload": Object { + "DaysAfterInitiation": 1, + }, + "ExpirationInDays": 1825, + "ExpiredObjectDeleteMarker": false, + "Id": Object { + "Fn::Join": Array [ + "", + Array [ + "LifecycleRuleaws-accelerator-s3-logs-", + Object { + "Ref": "AWS::AccountId", + }, + "-", + Object { + "Ref": "AWS::Region", + }, + ], + ], + }, + "NoncurrentVersionExpirationInDays": 1825, + "NoncurrentVersionTransitions": Array [ + Object { + "StorageClass": "DEEP_ARCHIVE", + "TransitionInDays": 366, + }, + ], + "Status": "Enabled", + "Transitions": Array [ + Object { + "StorageClass": "DEEP_ARCHIVE", + "TransitionInDays": 365, + }, + ], + }, + ], + }, + "OwnershipControls": Object { + "Rules": Array [ + Object { + "ObjectOwnership": "BucketOwnerPreferred", + }, + ], + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "InstallerAccessLogsBucketName4F700F48": Object { + "Properties": Object { + "Name": "/accelerator/installer-access-logs-bucket-name", + "Type": "String", + "Value": Object { + "Ref": "InstallerAccessLogsBucket647700E9", + }, + }, + "Type": "AWS::SSM::Parameter", + }, + "InstallerAccessLogsBucketPolicy20D4E285": Object { + "Properties": Object { + "Bucket": Object { + "Ref": "InstallerAccessLogsBucket647700E9", + }, + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "s3:*", + "Condition": Object { + "Bool": Object { + "aws:SecureTransport": "false", + }, + }, + "Effect": "Deny", + "Principal": Object { + "AWS": "*", + }, + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "InstallerAccessLogsBucket647700E9", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "InstallerAccessLogsBucket647700E9", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + "Sid": "deny-insecure-connections", + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::S3::BucketPolicy", + }, + "InstallerAdminRole7DEE4AC8": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "codebuild.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/AdministratorAccess", + ], + ], + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "InstallerAdminRoleDefaultPolicy7EEE1AAB": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/codebuild/", + Object { + "Ref": "InstallerProject879FF821", + }, + ], + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/codebuild/", + Object { + "Ref": "InstallerProject879FF821", + }, + ":*", + ], + ], + }, + ], + }, + Object { + "Action": Array [ + "codebuild:CreateReportGroup", + "codebuild:CreateReport", + "codebuild:UpdateReport", + "codebuild:BatchPutTestCases", + "codebuild:BatchPutCodeCoverages", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":codebuild:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":report-group/", + Object { + "Ref": "InstallerProject879FF821", + }, + "-*", + ], + ], + }, + }, + Object { + "Action": Array [ + "kms:Decrypt", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + }, + Object { + "Action": Array [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "SecureBucket747CD8C0", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "SecureBucket747CD8C0", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + Object { + "Action": Array [ + "kms:Decrypt", + "kms:DescribeKey", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "InstallerAdminRoleDefaultPolicy7EEE1AAB", + "Roles": Array [ + Object { + "Ref": "InstallerAdminRole7DEE4AC8", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "InstallerKey2A6A8C6D": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "F76", + "reason": "KMS key using * principal with added arn condition", + }, + ], + }, + }, + "Properties": Object { + "Description": "AWS Accelerator Management Account Kms Key", + "EnableKeyRotation": true, + "KeyPolicy": Object { + "Statement": Array [ + Object { + "Action": "kms:*", + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::", + Object { + "Ref": "AWS::AccountId", + }, + ":root", + ], + ], + }, + }, + "Resource": "*", + }, + Object { + "Action": Array [ + "kms:Encrypt", + "kms:Decrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:DescribeKey", + ], + "Condition": Object { + "ArnLike": Object { + "aws:PrincipalARN": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::", + Object { + "Ref": "AWS::AccountId", + }, + ":role/AWSAccelerator-*", + ], + ], + }, + }, + }, + "Effect": "Allow", + "Principal": Object { + "AWS": "*", + }, + "Resource": "*", + "Sid": "Allow Accelerator Role to use the encryption key", + }, + Object { + "Action": Array [ + "kms:Encrypt", + "kms:Decrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:DescribeKey", + ], + "Effect": "Allow", + "Principal": Object { + "Service": "sns.amazonaws.com", + }, + "Resource": "*", + "Sid": "Allow SNS service to use the encryption key", + }, + Object { + "Fn::If": Array [ + "IsCommercialCondition", + Object { + "Action": Array [ + "kms:GenerateDataKey*", + "kms:Decrypt", + ], + "Condition": Object { + "StringEquals": Object { + "kms:ViaService": Object { + "Fn::Join": Array [ + "", + Array [ + "sns.", + Object { + "Ref": "AWS::Region", + }, + ".amazonaws.com", + ], + ], + }, + }, + }, + "Effect": "Allow", + "Principal": Object { + "Service": "codestar-notifications.amazonaws.com", + }, + "Resource": "*", + "Sid": "KMS key access to codestar-notifications", + }, + Object { + "Ref": "AWS::NoValue", + }, + ], + }, + ], + }, + }, + "Type": "AWS::KMS::Key", + "UpdateReplacePolicy": "Retain", + }, + "InstallerKeyAliasD5C174F0": Object { + "Properties": Object { + "AliasName": "alias/accelerator/installer/kms/key", + "TargetKeyId": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + }, + "Type": "AWS::KMS::Alias", + }, + "InstallerProject879FF821": Object { + "Properties": Object { + "Artifacts": Object { + "Type": "CODEPIPELINE", + }, + "Cache": Object { + "Type": "NO_CACHE", + }, + "EncryptionKey": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + "Environment": Object { + "ComputeType": "BUILD_GENERAL1_MEDIUM", + "EnvironmentVariables": Array [ + Object { + "Name": "NODE_OPTIONS", + "Type": "PLAINTEXT", + "Value": "--max_old_space_size=4096", + }, + Object { + "Name": "CDK_NEW_BOOTSTRAP", + "Type": "PLAINTEXT", + "Value": "1", + }, + Object { + "Name": "ACCELERATOR_REPOSITORY_SOURCE", + "Type": "PLAINTEXT", + "Value": Object { + "Ref": "RepositorySource", + }, + }, + Object { + "Name": "ACCELERATOR_REPOSITORY_OWNER", + "Type": "PLAINTEXT", + "Value": Object { + "Ref": "RepositoryOwner", + }, + }, + Object { + "Name": "ACCELERATOR_REPOSITORY_NAME", + "Type": "PLAINTEXT", + "Value": Object { + "Ref": "RepositoryName", + }, + }, + Object { + "Name": "ACCELERATOR_REPOSITORY_BRANCH_NAME", + "Type": "PLAINTEXT", + "Value": Object { + "Ref": "RepositoryBranchName", + }, + }, + Object { + "Name": "ACCELERATOR_ENABLE_APPROVAL_STAGE", + "Type": "PLAINTEXT", + "Value": Object { + "Ref": "EnableApprovalStage", + }, + }, + Object { + "Name": "APPROVAL_STAGE_NOTIFY_EMAIL_LIST", + "Type": "PLAINTEXT", + "Value": Object { + "Fn::Join": Array [ + ",", + Object { + "Ref": "ApprovalStageNotifyEmailList", + }, + ], + }, + }, + Object { + "Name": "MANAGEMENT_ACCOUNT_EMAIL", + "Type": "PLAINTEXT", + "Value": Object { + "Ref": "ManagementAccountEmail", + }, + }, + Object { + "Name": "LOG_ARCHIVE_ACCOUNT_EMAIL", + "Type": "PLAINTEXT", + "Value": Object { + "Ref": "LogArchiveAccountEmail", + }, + }, + Object { + "Name": "AUDIT_ACCOUNT_EMAIL", + "Type": "PLAINTEXT", + "Value": Object { + "Ref": "AuditAccountEmail", + }, + }, + Object { + "Name": "ENABLE_TESTER", + "Type": "PLAINTEXT", + "Value": "true", + }, + Object { + "Name": "MANAGEMENT_CROSS_ACCOUNT_ROLE_NAME", + "Type": "PLAINTEXT", + "Value": "AWSControlTowerExecution", + }, + ], + "Image": "aws/codebuild/standard:5.0", + "ImagePullCredentialsType": "CODEBUILD", + "PrivilegedMode": true, + "Type": "LINUX_CONTAINER", + }, + "Name": "AWSAccelerator-InstallerProject", + "ServiceRole": Object { + "Fn::GetAtt": Array [ + "InstallerAdminRole7DEE4AC8", + "Arn", + ], + }, + "Source": Object { + "BuildSpec": Object { + "Fn::Join": Array [ + "", + Array [ + "version: \\"0.2\\" +phases: + install: + runtime-versions: + nodejs: 14 + pre_build: + commands: + - ENABLE_EXTERNAL_PIPELINE_ACCOUNT=\\"no\\" + - if [ ! -z \\"$MANAGEMENT_ACCOUNT_ID\\" ] && [ ! -z \\"$MANAGEMENT_ACCOUNT_ROLE_NAME\\" ]; then ENABLE_EXTERNAL_PIPELINE_ACCOUNT=\\"yes\\"; fi + build: + commands: + - cd source + - yarn install + - yarn lerna link + - yarn build + - cd packages/@aws-accelerator/installer + - yarn run cdk bootstrap --toolkitStackName AWSAccelerator-CDKToolkit aws://", + Object { + "Ref": "AWS::AccountId", + }, + "/", + Object { + "Ref": "AWS::Region", + }, + " --qualifier accel + - yarn run cdk bootstrap --toolkitStackName AWSAccelerator-CDKToolkit aws://", + Object { + "Ref": "AWS::AccountId", + }, + "/", + Object { + "Fn::FindInMap": Array [ + "GlobalRegionMap", + Object { + "Ref": "AWS::Partition", + }, + "regionName", + ], + }, + " --qualifier accel + - |- + if [ $ENABLE_EXTERNAL_PIPELINE_ACCOUNT = \\"yes\\" ]; then + export $(printf \\"AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s\\" $(aws sts assume-role --role-arn arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::\\"$MANAGEMENT_ACCOUNT_ID\\":role/\\"$MANAGEMENT_ACCOUNT_ROLE_NAME\\" --role-session-name acceleratorAssumeRoleSession --query \\"Credentials.[AccessKeyId,SecretAccessKey,SessionToken]\\" --output text)); + yarn run cdk bootstrap --toolkitStackName AWSAccelerator-CDKToolkit aws://$MANAGEMENT_ACCOUNT_ID/", + Object { + "Ref": "AWS::Region", + }, + " --qualifier accel; + yarn run cdk bootstrap --toolkitStackName AWSAccelerator-CDKToolkit aws://$MANAGEMENT_ACCOUNT_ID/", + Object { + "Fn::FindInMap": Array [ + "GlobalRegionMap", + Object { + "Ref": "AWS::Partition", + }, + "regionName", + ], + }, + " --qualifier accel; + unset AWS_ACCESS_KEY_ID; + unset AWS_SECRET_ACCESS_KEY; + unset AWS_SESSION_TOKEN; + fi + - cd ../accelerator + - yarn run ts-node --transpile-only cdk.ts deploy --require-approval never --stage pipeline --account ", + Object { + "Ref": "AWS::AccountId", + }, + " --region ", + Object { + "Ref": "AWS::Region", + }, + " --partition ", + Object { + "Ref": "AWS::Partition", + }, + " + - if [ \\"$ENABLE_TESTER\\" = \\"true\\" ]; then yarn run ts-node --transpile-only cdk.ts deploy --require-approval never --stage tester-pipeline --account ", + Object { + "Ref": "AWS::AccountId", + }, + " --region ", + Object { + "Ref": "AWS::Region", + }, + "; fi +", + ], + ], + }, + "Type": "CODEPIPELINE", + }, + }, + "Type": "AWS::CodeBuild::Project", + }, + "SecureBucket747CD8C0": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "KMSMasterKeyID": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + "SSEAlgorithm": "aws:kms", + }, + }, + ], + }, + "BucketName": Object { + "Fn::Join": Array [ + "", + Array [ + "aws-accelerator-installer-", + Object { + "Ref": "AWS::AccountId", + }, + "-", + Object { + "Ref": "AWS::Region", + }, + ], + ], + }, + "LifecycleConfiguration": Object { + "Rules": Array [ + Object { + "AbortIncompleteMultipartUpload": Object { + "DaysAfterInitiation": 1, + }, + "ExpirationInDays": 1825, + "ExpiredObjectDeleteMarker": false, + "Id": Object { + "Fn::Join": Array [ + "", + Array [ + "LifecycleRuleaws-accelerator-installer-", + Object { + "Ref": "AWS::AccountId", + }, + "-", + Object { + "Ref": "AWS::Region", + }, + ], + ], + }, + "NoncurrentVersionExpirationInDays": 1825, + "NoncurrentVersionTransitions": Array [ + Object { + "StorageClass": "DEEP_ARCHIVE", + "TransitionInDays": 366, + }, + ], + "Status": "Enabled", + "Transitions": Array [ + Object { + "StorageClass": "DEEP_ARCHIVE", + "TransitionInDays": 365, + }, + ], + }, + ], + }, + "LoggingConfiguration": Object { + "DestinationBucketName": Object { + "Ref": "InstallerAccessLogsBucket647700E9", + }, + "LogFilePrefix": Object { + "Fn::Join": Array [ + "", + Array [ + "aws-accelerator-installer-", + Object { + "Ref": "AWS::AccountId", + }, + "-", + Object { + "Ref": "AWS::Region", + }, + "/", + ], + ], + }, + }, + "OwnershipControls": Object { + "Rules": Array [ + Object { + "ObjectOwnership": "BucketOwnerPreferred", + }, + ], + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "SecureBucketPolicy6374AC61": Object { + "Properties": Object { + "Bucket": Object { + "Ref": "SecureBucket747CD8C0", + }, + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "s3:*", + "Condition": Object { + "Bool": Object { + "aws:SecureTransport": "false", + }, + }, + "Effect": "Deny", + "Principal": Object { + "AWS": "*", + }, + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "SecureBucket747CD8C0", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "SecureBucket747CD8C0", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + "Sid": "deny-insecure-connections", + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::S3::BucketPolicy", + }, + "SolutionHelper4825923B": Object { + "Condition": "SolutionHelperAnonymousDataToAWS62E4FDE2", + "DependsOn": Array [ + "SolutionHelperServiceRoleF70C0E2A", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W58", + "reason": "CloudWatch Logs are enabled in AWSLambdaBasicExecutionRole", + }, + Object { + "id": "W89", + "reason": "This function supports infrastructure deployment and is not deployed inside a VPC.", + }, + Object { + "id": "W92", + "reason": "This function supports infrastructure deployment and does not require setting ReservedConcurrentExecutions.", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "ZipFile": " + const AWS = require('aws-sdk'); + const response = require('cfn-response'); + const https = require('https'); + + async function post(url, data) { + const dataString = JSON.stringify(data) + const options = { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + timeout: 1000, // in ms + } + + return new Promise((resolve, reject) => { + const req = https.request(url, options, (res) => { + if (res.statusCode < 200 || res.statusCode > 299) { + return reject(new Error('HTTP status code: ', res.statusCode)) + } + const body = [] + res.on('data', (chunk) => body.push(chunk)) + res.on('end', () => { + const resString = Buffer.concat(body).toString() + resolve(resString) + }) + }) + req.on('error', (err) => { + reject(err) + }) + req.on('timeout', () => { + req.destroy() + reject(new Error('Request time out')) + }) + req.write(dataString) + req.end() + }) + } + + function uuidv4() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + } + + + function sanatizeData(resourceProperties) { + const keysToExclude = ['ServiceToken', 'Resource', 'SolutionId', 'UUID']; + return Object.keys(resourceProperties).reduce((sanatizedData, key) => { + if (!keysToExclude.includes(key)) { + sanatizedData[key] = resourceProperties[key]; + } + return sanatizedData; + }, {}) + } + + exports.handler = async function (event, context) { + console.log(JSON.stringify(event, null, 4)); + const requestType = event.RequestType; + const resourceProperties = event.ResourceProperties; + const resource = resourceProperties.Resource; + let data = {}; + try { + if (resource === 'UUID' && requestType === 'Create') { + data['UUID'] = uuidv4(); + } + if (resource === 'AnonymousMetric') { + const currentDate = new Date() + data = sanatizeData(resourceProperties); + data['RequestType'] = requestType; + const payload = { + Solution: resourceProperties.SolutionId, + UUID: resourceProperties.UUID, + TimeStamp: currentDate.toISOString(), + Data: data + } + + console.log('Sending metrics data: ', JSON.stringify(payload, null, 2)); + await post('https://metrics.awssolutionsbuilder.com/generic', payload); + console.log('Sent Data'); + } + } catch (error) { + console.log(error); + } + + await response.send(event, context, response.SUCCESS, data); + return; + } + ", + }, + "Description": "This function generates UUID for each deployment and sends anonymous data to the AWS Solutions team", + "FunctionName": "LandingZoneAccelerator-SolutionHelper", + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "SolutionHelperServiceRoleF70C0E2A", + "Arn", + ], + }, + "Runtime": "nodejs14.x", + "Timeout": 30, + }, + "Type": "AWS::Lambda::Function", + }, + "SolutionHelperCreateUniqueIDAF03406A": Object { + "Condition": "SolutionHelperAnonymousDataToAWS62E4FDE2", + "DeletionPolicy": "Delete", + "Properties": Object { + "Resource": "UUID", + "ServiceToken": Object { + "Fn::GetAtt": Array [ + "SolutionHelper4825923B", + "Arn", + ], + }, + }, + "Type": "Custom::CreateUUID", + "UpdateReplacePolicy": "Delete", + }, + "SolutionHelperSendAnonymousData9B19E31D": Object { + "Condition": "SolutionHelperAnonymousDataToAWS62E4FDE2", + "DeletionPolicy": "Delete", + "Properties": Object { + "BranchName": Object { + "Ref": "RepositoryBranchName", + }, + "Region": Object { + "Ref": "AWS::Region", + }, + "RepositoryName": Object { + "Ref": "RepositoryName", + }, + "RepositoryOwner": Object { + "Ref": "RepositoryOwner", + }, + "RepositorySource": Object { + "Ref": "RepositorySource", + }, + "Resource": "AnonymousMetric", + "ServiceToken": Object { + "Fn::GetAtt": Array [ + "SolutionHelper4825923B", + "Arn", + ], + }, + "SolutionId": "SO0199", + "UUID": Object { + "Fn::GetAtt": Array [ + "SolutionHelperCreateUniqueIDAF03406A", + "UUID", + ], + }, + }, + "Type": "Custom::AnonymousData", + "UpdateReplacePolicy": "Delete", + }, + "SolutionHelperServiceRoleF70C0E2A": Object { + "Metadata": Object { + "cdk_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "AwsSolutions-IAM4", + "reason": "Needed to write to CWL group", + }, + ], + }, + }, + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ], + ], + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "SsmParamAcceleratorVersionFF83282D": Object { + "Properties": Object { + "Name": "/accelerator/AWSAccelerator-Test-InstallerStack/version", + "Type": "String", + "Value": "1.0.0", + }, + "Type": "AWS::SSM::Parameter", + }, + "SsmParamStackId521A78D3": Object { + "Properties": Object { + "Name": "/accelerator/AWSAccelerator-Test-InstallerStack/stack-id", + "Type": "String", + "Value": Object { + "Ref": "AWS::StackId", + }, + }, + "Type": "AWS::SSM::Parameter", + }, + }, +} +`; + +exports[`InstallerStack Stack(installer): Snapshot test - management account stack without tester pipeline enabled 1`] = ` +Object { + "Conditions": Object { + "IsCommercialCondition": Object { + "Fn::Equals": Array [ + Object { + "Ref": "AWS::Partition", + }, + "aws", + ], + }, + "SolutionHelperAnonymousDataToAWS62E4FDE2": Object { + "Fn::Equals": Array [ + Object { + "Fn::FindInMap": Array [ + "SolutionHelperAnonymousData14B64A81", + "SendAnonymousData", + "Data", + ], + }, + "Yes", + ], + }, + "UseCodeCommitCondition": Object { + "Fn::Equals": Array [ + Object { + "Ref": "RepositorySource", + }, + "codecommit", + ], + }, + "UseGitHubCondition": Object { + "Fn::Equals": Array [ + Object { + "Ref": "RepositorySource", + }, + "github", + ], + }, + }, + "Mappings": Object { + "GlobalRegionMap": Object { + "aws": Object { + "regionName": "us-east-1", + }, + "aws-iso": Object { + "regionName": "us-iso-east-1", + }, + "aws-iso-b": Object { + "regionName": "us-isob-east-1", + }, + "aws-us-gov": Object { + "regionName": "us-gov-west-1", + }, + }, + "SolutionHelperAnonymousData14B64A81": Object { + "SendAnonymousData": Object { + "Data": "Yes", + }, + }, + }, + "Metadata": Object { + "AWS::CloudFormation::Interface": Object { + "ParameterGroups": Array [ + Object { + "Label": Object { + "default": "Git Repository Configuration", + }, + "Parameters": Array [ + "RepositorySource", + "RepositoryOwner", + "RepositoryName", + "RepositoryBranchName", + ], + }, + Object { + "Label": Object { + "default": "Pipeline Configuration", + }, + "Parameters": Array [ + "EnableApprovalStage", + "ApprovalStageNotifyEmailList", + ], + }, + Object { + "Label": Object { + "default": "Mandatory Accounts Configuration", + }, + "Parameters": Array [ + "ManagementAccountEmail", + "LogArchiveAccountEmail", + "AuditAccountEmail", + ], + }, + ], + "ParameterLabels": Object { + "ApprovalStageNotifyEmailList": Object { + "default": "Manual Approval Stage notification email list", + }, + "AuditAccountEmail": Object { + "default": "Audit Account Email", + }, + "EnableApprovalStage": Object { + "default": "Enable Approval Stage", + }, + "LogArchiveAccountEmail": Object { + "default": "Log Archive Account Email", + }, + "ManagementAccountEmail": Object { + "default": "Management Account Email", + }, + "RepositoryBranchName": Object { + "default": "Branch Name", + }, + "RepositoryName": Object { + "default": "Repository Name", + }, + "RepositoryOwner": Object { + "default": "Repository Owner", + }, + "RepositorySource": Object { + "default": "Source", + }, + }, + }, + }, + "Parameters": Object { + "ApprovalStageNotifyEmailList": Object { + "Description": "Provide comma(,) separated list of email ids to receive manual approval stage notification email", + "Type": "CommaDelimitedList", + }, + "AuditAccountEmail": Object { + "Description": "The security audit account (also referred to as the audit account)", + "Type": "String", + }, + "EnableApprovalStage": Object { + "AllowedValues": Array [ + "Yes", + "No", + ], + "Default": "Yes", + "Description": "Select yes to add a Manual Approval stage to accelerator pipeline", + "Type": "String", + }, + "LogArchiveAccountEmail": Object { + "Description": "The log archive account email", + "Type": "String", + }, + "ManagementAccountEmail": Object { + "Description": "The management (primary) account email", + "Type": "String", + }, + "RepositoryBranchName": Object { + "Description": "The name of the git branch to use for installation", + "Type": "String", + }, + "RepositoryName": Object { + "Default": "landing-zone-accelerator-on-aws", + "Description": "The name of the git repository hosting the accelerator code", + "Type": "String", + }, + "RepositoryOwner": Object { + "Default": "awslabs", + "Description": "The owner of the repository containing the accelerator code. (GitHub Only)", + "Type": "String", + }, + "RepositorySource": Object { + "AllowedValues": Array [ + "github", + "codecommit", + ], + "Default": "github", + "Description": "Specify the git host", + "Type": "String", + }, + }, + "Resources": Object { + "AcceleratorManagementKmsArnParameter1E6975BF": Object { + "Properties": Object { + "Name": "/accelerator/installer/kms/key-arn", + "Type": "String", + "Value": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + }, + "Type": "AWS::SSM::Parameter", + }, + "CodeCommitPipeline2208527B": Object { + "Condition": "UseCodeCommitCondition", + "DependsOn": Array [ + "CodeCommitPipelineRoleDefaultPolicyDE8B332B", + "CodeCommitPipelineRole5C35E76C", + ], + "Properties": Object { + "ArtifactStore": Object { + "EncryptionKey": Object { + "Id": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + "Type": "KMS", + }, + "Location": Object { + "Ref": "SecureBucket747CD8C0", + }, + "Type": "S3", + }, + "Name": "AWSAccelerator-Installer", + "RestartExecutionOnUpdate": true, + "RoleArn": Object { + "Fn::GetAtt": Array [ + "CodeCommitPipelineRole5C35E76C", + "Arn", + ], + }, + "Stages": Array [ + Object { + "Actions": Array [ + Object { + "ActionTypeId": Object { + "Category": "Source", + "Owner": "AWS", + "Provider": "CodeCommit", + "Version": "1", + }, + "Configuration": Object { + "BranchName": Object { + "Ref": "RepositoryBranchName", + }, + "PollForSourceChanges": false, + "RepositoryName": Object { + "Ref": "RepositoryName", + }, + }, + "Name": "Source", + "OutputArtifacts": Array [ + Object { + "Name": "Source", + }, + ], + "RoleArn": Object { + "Fn::GetAtt": Array [ + "CodeCommitPipelineSourceCodePipelineActionRoleFB176191", + "Arn", + ], + }, + "RunOrder": 1, + }, + ], + "Name": "Source", + }, + Object { + "Actions": Array [ + Object { + "ActionTypeId": Object { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1", + }, + "Configuration": Object { + "ProjectName": Object { + "Ref": "InstallerProject879FF821", + }, + }, + "InputArtifacts": Array [ + Object { + "Name": "Source", + }, + ], + "Name": "Install", + "RoleArn": Object { + "Fn::GetAtt": Array [ + "CodeCommitPipelineRole5C35E76C", + "Arn", + ], + }, + "RunOrder": 1, + }, + ], + "Name": "Install", + }, + ], + }, + "Type": "AWS::CodePipeline::Pipeline", + }, + "CodeCommitPipelineRole5C35E76C": Object { + "Condition": "UseCodeCommitCondition", + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "codepipeline.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::IAM::Role", + }, + "CodeCommitPipelineRoleDefaultPolicyDE8B332B": Object { + "Condition": "UseCodeCommitCondition", + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", + "s3:Abort*", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "SecureBucket747CD8C0", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "SecureBucket747CD8C0", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + Object { + "Action": Array [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + }, + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "CodeCommitPipelineSourceCodePipelineActionRoleFB176191", + "Arn", + ], + }, + }, + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "CodeCommitPipelineRole5C35E76C", + "Arn", + ], + }, + }, + Object { + "Action": Array [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "InstallerProject879FF821", + "Arn", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "CodeCommitPipelineRoleDefaultPolicyDE8B332B", + "Roles": Array [ + Object { + "Ref": "CodeCommitPipelineRole5C35E76C", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "CodeCommitPipelineSourceCodePipelineActionRoleDefaultPolicyF71E0C0D": Object { + "Condition": "UseCodeCommitCondition", + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", + "s3:Abort*", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "SecureBucket747CD8C0", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "SecureBucket747CD8C0", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + Object { + "Action": Array [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + }, + Object { + "Action": Array [ + "codecommit:GetBranch", + "codecommit:GetCommit", + "codecommit:UploadArchive", + "codecommit:GetUploadArchiveStatus", + "codecommit:CancelUploadArchive", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":codecommit:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "RepositoryName", + }, + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "CodeCommitPipelineSourceCodePipelineActionRoleDefaultPolicyF71E0C0D", + "Roles": Array [ + Object { + "Ref": "CodeCommitPipelineSourceCodePipelineActionRoleFB176191", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "CodeCommitPipelineSourceCodePipelineActionRoleFB176191": Object { + "Condition": "UseCodeCommitCondition", + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::", + Object { + "Ref": "AWS::AccountId", + }, + ":root", + ], + ], + }, + }, + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::IAM::Role", + }, + "GitHubPipeline7B79E906": Object { + "Condition": "UseGitHubCondition", + "DependsOn": Array [ + "GitHubPipelineRoleDefaultPolicyD82457D6", + "GitHubPipelineRole6F4DEF1B", + ], + "Properties": Object { + "ArtifactStore": Object { + "EncryptionKey": Object { + "Id": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + "Type": "KMS", + }, + "Location": Object { + "Ref": "SecureBucket747CD8C0", + }, + "Type": "S3", + }, + "Name": "AWSAccelerator-Installer", + "RestartExecutionOnUpdate": true, + "RoleArn": Object { + "Fn::GetAtt": Array [ + "GitHubPipelineRole6F4DEF1B", + "Arn", + ], + }, + "Stages": Array [ + Object { + "Actions": Array [ + Object { + "ActionTypeId": Object { + "Category": "Source", + "Owner": "ThirdParty", + "Provider": "GitHub", + "Version": "1", + }, + "Configuration": Object { + "Branch": Object { + "Ref": "RepositoryBranchName", + }, + "OAuthToken": "{{resolve:secretsmanager:accelerator/github-token:SecretString:::}}", + "Owner": Object { + "Ref": "RepositoryOwner", + }, + "PollForSourceChanges": false, + "Repo": Object { + "Ref": "RepositoryName", + }, + }, + "Name": "Source", + "OutputArtifacts": Array [ + Object { + "Name": "Source", + }, + ], + "RunOrder": 1, + }, + ], + "Name": "Source", + }, + Object { + "Actions": Array [ + Object { + "ActionTypeId": Object { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1", + }, + "Configuration": Object { + "ProjectName": Object { + "Ref": "InstallerProject879FF821", + }, + }, + "InputArtifacts": Array [ + Object { + "Name": "Source", + }, + ], + "Name": "Install", + "RoleArn": Object { + "Fn::GetAtt": Array [ + "GitHubPipelineRole6F4DEF1B", + "Arn", + ], + }, + "RunOrder": 1, + }, + ], + "Name": "Install", + }, + ], + }, + "Type": "AWS::CodePipeline::Pipeline", + }, + "GitHubPipelineRole6F4DEF1B": Object { + "Condition": "UseGitHubCondition", + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "codepipeline.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::IAM::Role", + }, + "GitHubPipelineRoleDefaultPolicyD82457D6": Object { + "Condition": "UseGitHubCondition", + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", + "s3:Abort*", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "SecureBucket747CD8C0", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "SecureBucket747CD8C0", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + Object { + "Action": Array [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + }, + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "GitHubPipelineRole6F4DEF1B", + "Arn", + ], + }, + }, + Object { + "Action": Array [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "InstallerProject879FF821", + "Arn", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "GitHubPipelineRoleDefaultPolicyD82457D6", + "Roles": Array [ + Object { + "Ref": "GitHubPipelineRole6F4DEF1B", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "InstallerAccessLogsBucket647700E9": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cdk_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "AwsSolutions-S1", + "reason": "AccessLogsBucket has server access logs disabled till the task for access logging completed.", + }, + ], + }, + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W35", + "reason": "This is an access logging bucket.", + }, + ], + }, + }, + "Properties": Object { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "BucketName": Object { + "Fn::Join": Array [ + "", + Array [ + "aws-accelerator-s3-logs-", + Object { + "Ref": "AWS::AccountId", + }, + "-", + Object { + "Ref": "AWS::Region", + }, + ], + ], + }, + "LifecycleConfiguration": Object { + "Rules": Array [ + Object { + "AbortIncompleteMultipartUpload": Object { + "DaysAfterInitiation": 1, + }, + "ExpirationInDays": 1825, + "ExpiredObjectDeleteMarker": false, + "Id": Object { + "Fn::Join": Array [ + "", + Array [ + "LifecycleRuleaws-accelerator-s3-logs-", + Object { + "Ref": "AWS::AccountId", + }, + "-", + Object { + "Ref": "AWS::Region", + }, + ], + ], + }, + "NoncurrentVersionExpirationInDays": 1825, + "NoncurrentVersionTransitions": Array [ + Object { + "StorageClass": "DEEP_ARCHIVE", + "TransitionInDays": 366, + }, + ], + "Status": "Enabled", + "Transitions": Array [ + Object { + "StorageClass": "DEEP_ARCHIVE", + "TransitionInDays": 365, + }, + ], + }, + ], + }, + "OwnershipControls": Object { + "Rules": Array [ + Object { + "ObjectOwnership": "BucketOwnerPreferred", + }, + ], + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "InstallerAccessLogsBucketName4F700F48": Object { + "Properties": Object { + "Name": "/accelerator/installer-access-logs-bucket-name", + "Type": "String", + "Value": Object { + "Ref": "InstallerAccessLogsBucket647700E9", + }, + }, + "Type": "AWS::SSM::Parameter", + }, + "InstallerAccessLogsBucketPolicy20D4E285": Object { + "Properties": Object { + "Bucket": Object { + "Ref": "InstallerAccessLogsBucket647700E9", + }, + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "s3:*", + "Condition": Object { + "Bool": Object { + "aws:SecureTransport": "false", + }, + }, + "Effect": "Deny", + "Principal": Object { + "AWS": "*", + }, + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "InstallerAccessLogsBucket647700E9", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "InstallerAccessLogsBucket647700E9", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + "Sid": "deny-insecure-connections", + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::S3::BucketPolicy", + }, + "InstallerAdminRole7DEE4AC8": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "codebuild.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/AdministratorAccess", + ], + ], + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "InstallerAdminRoleDefaultPolicy7EEE1AAB": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/codebuild/", + Object { + "Ref": "InstallerProject879FF821", + }, + ], + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/codebuild/", + Object { + "Ref": "InstallerProject879FF821", + }, + ":*", + ], + ], + }, + ], + }, + Object { + "Action": Array [ + "codebuild:CreateReportGroup", + "codebuild:CreateReport", + "codebuild:UpdateReport", + "codebuild:BatchPutTestCases", + "codebuild:BatchPutCodeCoverages", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":codebuild:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":report-group/", + Object { + "Ref": "InstallerProject879FF821", + }, + "-*", + ], + ], + }, + }, + Object { + "Action": Array [ + "kms:Decrypt", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + }, + Object { + "Action": Array [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "SecureBucket747CD8C0", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "SecureBucket747CD8C0", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + Object { + "Action": Array [ + "kms:Decrypt", + "kms:DescribeKey", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "InstallerAdminRoleDefaultPolicy7EEE1AAB", + "Roles": Array [ + Object { + "Ref": "InstallerAdminRole7DEE4AC8", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "InstallerKey2A6A8C6D": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "F76", + "reason": "KMS key using * principal with added arn condition", + }, + ], + }, + }, + "Properties": Object { + "Description": "AWS Accelerator Management Account Kms Key", + "EnableKeyRotation": true, + "KeyPolicy": Object { + "Statement": Array [ + Object { + "Action": "kms:*", + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::", + Object { + "Ref": "AWS::AccountId", + }, + ":root", + ], + ], + }, + }, + "Resource": "*", + }, + Object { + "Action": Array [ + "kms:Encrypt", + "kms:Decrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:DescribeKey", + ], + "Condition": Object { + "ArnLike": Object { + "aws:PrincipalARN": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::", + Object { + "Ref": "AWS::AccountId", + }, + ":role/AWSAccelerator-*", + ], + ], + }, + }, + }, + "Effect": "Allow", + "Principal": Object { + "AWS": "*", + }, + "Resource": "*", + "Sid": "Allow Accelerator Role to use the encryption key", + }, + Object { + "Action": Array [ + "kms:Encrypt", + "kms:Decrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:DescribeKey", + ], + "Effect": "Allow", + "Principal": Object { + "Service": "sns.amazonaws.com", + }, + "Resource": "*", + "Sid": "Allow SNS service to use the encryption key", + }, + Object { + "Fn::If": Array [ + "IsCommercialCondition", + Object { + "Action": Array [ + "kms:GenerateDataKey*", + "kms:Decrypt", + ], + "Condition": Object { + "StringEquals": Object { + "kms:ViaService": Object { + "Fn::Join": Array [ + "", + Array [ + "sns.", + Object { + "Ref": "AWS::Region", + }, + ".amazonaws.com", + ], + ], + }, + }, + }, + "Effect": "Allow", + "Principal": Object { + "Service": "codestar-notifications.amazonaws.com", + }, + "Resource": "*", + "Sid": "KMS key access to codestar-notifications", + }, + Object { + "Ref": "AWS::NoValue", + }, + ], + }, + ], + }, + }, + "Type": "AWS::KMS::Key", + "UpdateReplacePolicy": "Retain", + }, + "InstallerKeyAliasD5C174F0": Object { + "Properties": Object { + "AliasName": "alias/accelerator/installer/kms/key", + "TargetKeyId": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + }, + "Type": "AWS::KMS::Alias", + }, + "InstallerProject879FF821": Object { + "Properties": Object { + "Artifacts": Object { + "Type": "CODEPIPELINE", + }, + "Cache": Object { + "Type": "NO_CACHE", + }, + "EncryptionKey": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + "Environment": Object { + "ComputeType": "BUILD_GENERAL1_MEDIUM", + "EnvironmentVariables": Array [ + Object { + "Name": "NODE_OPTIONS", + "Type": "PLAINTEXT", + "Value": "--max_old_space_size=4096", + }, + Object { + "Name": "CDK_NEW_BOOTSTRAP", + "Type": "PLAINTEXT", + "Value": "1", + }, + Object { + "Name": "ACCELERATOR_REPOSITORY_SOURCE", + "Type": "PLAINTEXT", + "Value": Object { + "Ref": "RepositorySource", + }, + }, + Object { + "Name": "ACCELERATOR_REPOSITORY_OWNER", + "Type": "PLAINTEXT", + "Value": Object { + "Ref": "RepositoryOwner", + }, + }, + Object { + "Name": "ACCELERATOR_REPOSITORY_NAME", + "Type": "PLAINTEXT", + "Value": Object { + "Ref": "RepositoryName", + }, + }, + Object { + "Name": "ACCELERATOR_REPOSITORY_BRANCH_NAME", + "Type": "PLAINTEXT", + "Value": Object { + "Ref": "RepositoryBranchName", + }, + }, + Object { + "Name": "ACCELERATOR_ENABLE_APPROVAL_STAGE", + "Type": "PLAINTEXT", + "Value": Object { + "Ref": "EnableApprovalStage", + }, + }, + Object { + "Name": "APPROVAL_STAGE_NOTIFY_EMAIL_LIST", + "Type": "PLAINTEXT", + "Value": Object { + "Fn::Join": Array [ + ",", + Object { + "Ref": "ApprovalStageNotifyEmailList", + }, + ], + }, + }, + Object { + "Name": "MANAGEMENT_ACCOUNT_EMAIL", + "Type": "PLAINTEXT", + "Value": Object { + "Ref": "ManagementAccountEmail", + }, + }, + Object { + "Name": "LOG_ARCHIVE_ACCOUNT_EMAIL", + "Type": "PLAINTEXT", + "Value": Object { + "Ref": "LogArchiveAccountEmail", + }, + }, + Object { + "Name": "AUDIT_ACCOUNT_EMAIL", + "Type": "PLAINTEXT", + "Value": Object { + "Ref": "AuditAccountEmail", + }, + }, + ], + "Image": "aws/codebuild/standard:5.0", + "ImagePullCredentialsType": "CODEBUILD", + "PrivilegedMode": true, + "Type": "LINUX_CONTAINER", + }, + "Name": "AWSAccelerator-InstallerProject", + "ServiceRole": Object { + "Fn::GetAtt": Array [ + "InstallerAdminRole7DEE4AC8", + "Arn", + ], + }, + "Source": Object { + "BuildSpec": Object { + "Fn::Join": Array [ + "", + Array [ + "version: \\"0.2\\" +phases: + install: + runtime-versions: + nodejs: 14 + pre_build: + commands: + - ENABLE_EXTERNAL_PIPELINE_ACCOUNT=\\"no\\" + - if [ ! -z \\"$MANAGEMENT_ACCOUNT_ID\\" ] && [ ! -z \\"$MANAGEMENT_ACCOUNT_ROLE_NAME\\" ]; then ENABLE_EXTERNAL_PIPELINE_ACCOUNT=\\"yes\\"; fi + build: + commands: + - cd source + - yarn install + - yarn lerna link + - yarn build + - cd packages/@aws-accelerator/installer + - yarn run cdk bootstrap --toolkitStackName AWSAccelerator-CDKToolkit aws://", + Object { + "Ref": "AWS::AccountId", + }, + "/", + Object { + "Ref": "AWS::Region", + }, + " --qualifier accel + - yarn run cdk bootstrap --toolkitStackName AWSAccelerator-CDKToolkit aws://", + Object { + "Ref": "AWS::AccountId", + }, + "/", + Object { + "Fn::FindInMap": Array [ + "GlobalRegionMap", + Object { + "Ref": "AWS::Partition", + }, + "regionName", + ], + }, + " --qualifier accel + - |- + if [ $ENABLE_EXTERNAL_PIPELINE_ACCOUNT = \\"yes\\" ]; then + export $(printf \\"AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s\\" $(aws sts assume-role --role-arn arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::\\"$MANAGEMENT_ACCOUNT_ID\\":role/\\"$MANAGEMENT_ACCOUNT_ROLE_NAME\\" --role-session-name acceleratorAssumeRoleSession --query \\"Credentials.[AccessKeyId,SecretAccessKey,SessionToken]\\" --output text)); + yarn run cdk bootstrap --toolkitStackName AWSAccelerator-CDKToolkit aws://$MANAGEMENT_ACCOUNT_ID/", + Object { + "Ref": "AWS::Region", + }, + " --qualifier accel; + yarn run cdk bootstrap --toolkitStackName AWSAccelerator-CDKToolkit aws://$MANAGEMENT_ACCOUNT_ID/", + Object { + "Fn::FindInMap": Array [ + "GlobalRegionMap", + Object { + "Ref": "AWS::Partition", + }, + "regionName", + ], + }, + " --qualifier accel; + unset AWS_ACCESS_KEY_ID; + unset AWS_SECRET_ACCESS_KEY; + unset AWS_SESSION_TOKEN; + fi + - cd ../accelerator + - yarn run ts-node --transpile-only cdk.ts deploy --require-approval never --stage pipeline --account ", + Object { + "Ref": "AWS::AccountId", + }, + " --region ", + Object { + "Ref": "AWS::Region", + }, + " --partition ", + Object { + "Ref": "AWS::Partition", + }, + " + - if [ \\"$ENABLE_TESTER\\" = \\"true\\" ]; then yarn run ts-node --transpile-only cdk.ts deploy --require-approval never --stage tester-pipeline --account ", + Object { + "Ref": "AWS::AccountId", + }, + " --region ", + Object { + "Ref": "AWS::Region", + }, + "; fi +", + ], + ], + }, + "Type": "CODEPIPELINE", + }, + }, + "Type": "AWS::CodeBuild::Project", + }, + "SecureBucket747CD8C0": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "KMSMasterKeyID": Object { + "Fn::GetAtt": Array [ + "InstallerKey2A6A8C6D", + "Arn", + ], + }, + "SSEAlgorithm": "aws:kms", + }, + }, + ], + }, + "BucketName": Object { + "Fn::Join": Array [ + "", + Array [ + "aws-accelerator-installer-", + Object { + "Ref": "AWS::AccountId", + }, + "-", + Object { + "Ref": "AWS::Region", + }, + ], + ], + }, + "LifecycleConfiguration": Object { + "Rules": Array [ + Object { + "AbortIncompleteMultipartUpload": Object { + "DaysAfterInitiation": 1, + }, + "ExpirationInDays": 1825, + "ExpiredObjectDeleteMarker": false, + "Id": Object { + "Fn::Join": Array [ + "", + Array [ + "LifecycleRuleaws-accelerator-installer-", + Object { + "Ref": "AWS::AccountId", + }, + "-", + Object { + "Ref": "AWS::Region", + }, + ], + ], + }, + "NoncurrentVersionExpirationInDays": 1825, + "NoncurrentVersionTransitions": Array [ + Object { + "StorageClass": "DEEP_ARCHIVE", + "TransitionInDays": 366, + }, + ], + "Status": "Enabled", + "Transitions": Array [ + Object { + "StorageClass": "DEEP_ARCHIVE", + "TransitionInDays": 365, + }, + ], + }, + ], + }, + "LoggingConfiguration": Object { + "DestinationBucketName": Object { + "Ref": "InstallerAccessLogsBucket647700E9", + }, + "LogFilePrefix": Object { + "Fn::Join": Array [ + "", + Array [ + "aws-accelerator-installer-", + Object { + "Ref": "AWS::AccountId", + }, + "-", + Object { + "Ref": "AWS::Region", + }, + "/", + ], + ], + }, + }, + "OwnershipControls": Object { + "Rules": Array [ + Object { + "ObjectOwnership": "BucketOwnerPreferred", + }, + ], + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "SecureBucketPolicy6374AC61": Object { + "Properties": Object { + "Bucket": Object { + "Ref": "SecureBucket747CD8C0", + }, + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "s3:*", + "Condition": Object { + "Bool": Object { + "aws:SecureTransport": "false", + }, + }, + "Effect": "Deny", + "Principal": Object { + "AWS": "*", + }, + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "SecureBucket747CD8C0", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "SecureBucket747CD8C0", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + "Sid": "deny-insecure-connections", + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::S3::BucketPolicy", + }, + "SolutionHelper4825923B": Object { + "Condition": "SolutionHelperAnonymousDataToAWS62E4FDE2", + "DependsOn": Array [ + "SolutionHelperServiceRoleF70C0E2A", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W58", + "reason": "CloudWatch Logs are enabled in AWSLambdaBasicExecutionRole", + }, + Object { + "id": "W89", + "reason": "This function supports infrastructure deployment and is not deployed inside a VPC.", + }, + Object { + "id": "W92", + "reason": "This function supports infrastructure deployment and does not require setting ReservedConcurrentExecutions.", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "ZipFile": " + const AWS = require('aws-sdk'); + const response = require('cfn-response'); + const https = require('https'); + + async function post(url, data) { + const dataString = JSON.stringify(data) + const options = { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + timeout: 1000, // in ms + } + + return new Promise((resolve, reject) => { + const req = https.request(url, options, (res) => { + if (res.statusCode < 200 || res.statusCode > 299) { + return reject(new Error('HTTP status code: ', res.statusCode)) + } + const body = [] + res.on('data', (chunk) => body.push(chunk)) + res.on('end', () => { + const resString = Buffer.concat(body).toString() + resolve(resString) + }) + }) + req.on('error', (err) => { + reject(err) + }) + req.on('timeout', () => { + req.destroy() + reject(new Error('Request time out')) + }) + req.write(dataString) + req.end() + }) + } + + function uuidv4() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + } + + + function sanatizeData(resourceProperties) { + const keysToExclude = ['ServiceToken', 'Resource', 'SolutionId', 'UUID']; + return Object.keys(resourceProperties).reduce((sanatizedData, key) => { + if (!keysToExclude.includes(key)) { + sanatizedData[key] = resourceProperties[key]; + } + return sanatizedData; + }, {}) + } + + exports.handler = async function (event, context) { + console.log(JSON.stringify(event, null, 4)); + const requestType = event.RequestType; + const resourceProperties = event.ResourceProperties; + const resource = resourceProperties.Resource; + let data = {}; + try { + if (resource === 'UUID' && requestType === 'Create') { + data['UUID'] = uuidv4(); + } + if (resource === 'AnonymousMetric') { + const currentDate = new Date() + data = sanatizeData(resourceProperties); + data['RequestType'] = requestType; + const payload = { + Solution: resourceProperties.SolutionId, + UUID: resourceProperties.UUID, + TimeStamp: currentDate.toISOString(), + Data: data + } + + console.log('Sending metrics data: ', JSON.stringify(payload, null, 2)); + await post('https://metrics.awssolutionsbuilder.com/generic', payload); + console.log('Sent Data'); + } + } catch (error) { + console.log(error); + } + + await response.send(event, context, response.SUCCESS, data); + return; + } + ", + }, + "Description": "This function generates UUID for each deployment and sends anonymous data to the AWS Solutions team", + "FunctionName": "LandingZoneAccelerator-SolutionHelper", + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "SolutionHelperServiceRoleF70C0E2A", + "Arn", + ], + }, + "Runtime": "nodejs14.x", + "Timeout": 30, + }, + "Type": "AWS::Lambda::Function", + }, + "SolutionHelperCreateUniqueIDAF03406A": Object { + "Condition": "SolutionHelperAnonymousDataToAWS62E4FDE2", + "DeletionPolicy": "Delete", + "Properties": Object { + "Resource": "UUID", + "ServiceToken": Object { + "Fn::GetAtt": Array [ + "SolutionHelper4825923B", + "Arn", + ], + }, + }, + "Type": "Custom::CreateUUID", + "UpdateReplacePolicy": "Delete", + }, + "SolutionHelperSendAnonymousData9B19E31D": Object { + "Condition": "SolutionHelperAnonymousDataToAWS62E4FDE2", + "DeletionPolicy": "Delete", + "Properties": Object { + "BranchName": Object { + "Ref": "RepositoryBranchName", + }, + "Region": Object { + "Ref": "AWS::Region", + }, + "RepositoryName": Object { + "Ref": "RepositoryName", + }, + "RepositoryOwner": Object { + "Ref": "RepositoryOwner", + }, + "RepositorySource": Object { + "Ref": "RepositorySource", + }, + "Resource": "AnonymousMetric", + "ServiceToken": Object { + "Fn::GetAtt": Array [ + "SolutionHelper4825923B", + "Arn", + ], + }, + "SolutionId": "SO0199", + "UUID": Object { + "Fn::GetAtt": Array [ + "SolutionHelperCreateUniqueIDAF03406A", + "UUID", + ], + }, + }, + "Type": "Custom::AnonymousData", + "UpdateReplacePolicy": "Delete", + }, + "SolutionHelperServiceRoleF70C0E2A": Object { + "Metadata": Object { + "cdk_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "AwsSolutions-IAM4", + "reason": "Needed to write to CWL group", + }, + ], + }, + }, + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ], + ], + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "SsmParamAcceleratorVersionFF83282D": Object { + "Properties": Object { + "Name": "/accelerator/AWSAccelerator-Test-InstallerStack/version", + "Type": "String", + "Value": "1.0.0", + }, + "Type": "AWS::SSM::Parameter", + }, + "SsmParamStackId521A78D3": Object { + "Properties": Object { + "Name": "/accelerator/AWSAccelerator-Test-InstallerStack/stack-id", + "Type": "String", + "Value": Object { + "Ref": "AWS::StackId", + }, + }, + "Type": "AWS::SSM::Parameter", + }, + }, +} +`; diff --git a/source/packages/@aws-accelerator/installer/test/installer.test.ts b/source/packages/@aws-accelerator/installer/test/installer.test.ts new file mode 100644 index 000000000..e2b2a076f --- /dev/null +++ b/source/packages/@aws-accelerator/installer/test/installer.test.ts @@ -0,0 +1,2439 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; + +import { SynthUtils } from '@aws-cdk/assert'; + +import { InstallerStack } from '../lib/installer-stack'; + +// Test prefix +const testNamePrefix = 'Stack(installer): '; + +//Initialize stack from management account with tester pipeline +const managementAccountStackWithTesterPipeline = new InstallerStack( + new cdk.App(), + 'AWSAccelerator-Test-InstallerStack', + { + synthesizer: new cdk.DefaultStackSynthesizer({ + generateBootstrapVersionRule: false, + }), + useExternalPipelineAccount: false, + enableTester: true, + managementCrossAccountRoleName: 'AWSControlTowerExecution', + }, +); + +// Initialize stack from management account without tester pipeline +const managementAccountStackWithoutTesterPipeline = new InstallerStack( + new cdk.App(), + 'AWSAccelerator-Test-InstallerStack', + { + synthesizer: new cdk.DefaultStackSynthesizer({ + generateBootstrapVersionRule: false, + }), + useExternalPipelineAccount: false, + enableTester: false, + }, +); + +//Initialize stack from external pipeline account with tester pipeline +const externalPipelineAccountStackWithTesterPipeline = new InstallerStack( + new cdk.App(), + 'AWSAccelerator-Test-InstallerStack', + { + synthesizer: new cdk.DefaultStackSynthesizer({ + generateBootstrapVersionRule: false, + }), + useExternalPipelineAccount: true, + enableTester: true, + managementCrossAccountRoleName: 'AWSControlTowerExecution', + }, +); + +//Initialize stack from external pipeline account without tester pipeline +const externalPipelineAccountStackWithoutTesterPipeline = new InstallerStack( + new cdk.App(), + 'AWSAccelerator-Test-InstallerStack', + { + synthesizer: new cdk.DefaultStackSynthesizer({ + generateBootstrapVersionRule: false, + }), + useExternalPipelineAccount: true, + enableTester: false, + }, +); + +/** + * CentralLogsBucket construct test + */ +describe('InstallerStack', () => { + /** + * Snapshot test - management account stack with tester pipeline enabled + */ + test(`${testNamePrefix} Snapshot test - management account stack with tester pipeline enabled`, () => { + expect(SynthUtils.toCloudFormation(managementAccountStackWithTesterPipeline)).toMatchSnapshot(); + }); + + /** + * Snapshot test - management account stack without tester pipeline enabled + */ + test(`${testNamePrefix} Snapshot test - management account stack without tester pipeline enabled`, () => { + expect(SynthUtils.toCloudFormation(managementAccountStackWithoutTesterPipeline)).toMatchSnapshot(); + }); + + /** + * Snapshot test - external pipeline account stack with tester pipeline enabled + */ + test(`${testNamePrefix} Snapshot test - external pipeline account stack with tester pipeline enabled`, () => { + expect(SynthUtils.toCloudFormation(externalPipelineAccountStackWithTesterPipeline)).toMatchSnapshot(); + }); + + /** + * Snapshot test - external pipeline account stack without tester pipeline enabled + */ + test(`${testNamePrefix} Snapshot test - external pipeline account stack without tester pipeline enabled`, () => { + expect(SynthUtils.toCloudFormation(externalPipelineAccountStackWithoutTesterPipeline)).toMatchSnapshot(); + }); + + // ************************************** + // Fine grained test cases + // ************************************** + + /** + * Management account pipeline stack - CloudFormation interface metadata test + */ + test(`${testNamePrefix} Management account pipeline stack - CloudFormation interface metadata test`, () => { + cdk.assertions.Template.fromStack(managementAccountStackWithTesterPipeline).templateMatches({ + Metadata: { + 'AWS::CloudFormation::Interface': { + ParameterGroups: [ + { + Label: { + default: 'Git Repository Configuration', + }, + Parameters: ['RepositorySource', 'RepositoryOwner', 'RepositoryName', 'RepositoryBranchName'], + }, + { + Label: { + default: 'Pipeline Configuration', + }, + Parameters: ['EnableApprovalStage', 'ApprovalStageNotifyEmailList'], + }, + { + Label: { + default: 'Mandatory Accounts Configuration', + }, + Parameters: ['ManagementAccountEmail', 'LogArchiveAccountEmail', 'AuditAccountEmail'], + }, + ], + ParameterLabels: { + EnableApprovalStage: { + default: 'Enable Approval Stage', + }, + ApprovalStageNotifyEmailList: { + default: 'Manual Approval Stage notification email list', + }, + RepositoryBranchName: { + default: 'Branch Name', + }, + RepositoryName: { + default: 'Repository Name', + }, + RepositoryOwner: { + default: 'Repository Owner', + }, + RepositorySource: { + default: 'Source', + }, + }, + }, + }, + }); + }); + + /** + * External pipeline account stack - CloudFormation interface metadata test + */ + test(`${testNamePrefix} External pipeline account stack - CloudFormation interface metadata test`, () => { + cdk.assertions.Template.fromStack(externalPipelineAccountStackWithTesterPipeline).templateMatches({ + Metadata: { + 'AWS::CloudFormation::Interface': { + ParameterGroups: [ + { + Label: { + default: 'Git Repository Configuration', + }, + Parameters: ['RepositorySource', 'RepositoryOwner', 'RepositoryName', 'RepositoryBranchName'], + }, + { + Label: { + default: 'Pipeline Configuration', + }, + Parameters: ['EnableApprovalStage', 'ApprovalStageNotifyEmailList'], + }, + { + Label: { + default: 'Mandatory Accounts Configuration', + }, + Parameters: ['ManagementAccountEmail', 'LogArchiveAccountEmail', 'AuditAccountEmail'], + }, + { + Label: { + default: 'Target Environment Configuration', + }, + Parameters: ['AcceleratorQualifier', 'ManagementAccountId', 'ManagementAccountRoleName'], + }, + ], + ParameterLabels: { + AuditAccountEmail: { + default: 'Audit Account Email', + }, + EnableApprovalStage: { + default: 'Enable Approval Stage', + }, + ApprovalStageNotifyEmailList: { + default: 'Manual Approval Stage notification email list', + }, + LogArchiveAccountEmail: { + default: 'Log Archive Account Email', + }, + ManagementAccountEmail: { + default: 'Management Account Email', + }, + RepositoryBranchName: { + default: 'Branch Name', + }, + RepositoryName: { + default: 'Repository Name', + }, + RepositoryOwner: { + default: 'Repository Owner', + }, + RepositorySource: { + default: 'Source', + }, + }, + }, + }, + }); + }); + + /** + * Management account pipeline stack - Installer project iam role resource test + */ + test(`${testNamePrefix} Management account pipeline stack - Installer project iam role resource test`, () => { + cdk.assertions.Template.fromStack(managementAccountStackWithTesterPipeline).templateMatches({ + Resources: { + InstallerAdminRole7DEE4AC8: { + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'codebuild.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::aws:policy/AdministratorAccess', + ], + ], + }, + ], + }, + Type: 'AWS::IAM::Role', + }, + }, + }); + }); + + /** + * External pipeline account stack - Installer project iam role resource test + */ + test(`${testNamePrefix} External pipeline account stack - Installer project iam role resource test`, () => { + cdk.assertions.Template.fromStack(externalPipelineAccountStackWithTesterPipeline).templateMatches({ + Resources: { + InstallerAdminRole7DEE4AC8: { + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'codebuild.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::aws:policy/AdministratorAccess', + ], + ], + }, + ], + }, + Type: 'AWS::IAM::Role', + }, + }, + }); + }); + + /** + * Management account pipeline stack with tester pipeline - Installer project resource test + */ + test(`${testNamePrefix} Management account pipeline stack with tester pipeline - Installer project resource test`, () => { + cdk.assertions.Template.fromStack(managementAccountStackWithTesterPipeline).templateMatches({ + Resources: { + InstallerProject879FF821: { + Type: 'AWS::CodeBuild::Project', + Properties: { + Artifacts: { + Type: 'CODEPIPELINE', + }, + Environment: { + ComputeType: 'BUILD_GENERAL1_MEDIUM', + EnvironmentVariables: [ + { + Name: 'NODE_OPTIONS', + Type: 'PLAINTEXT', + Value: '--max_old_space_size=4096', + }, + { + Name: 'CDK_NEW_BOOTSTRAP', + Type: 'PLAINTEXT', + Value: '1', + }, + { + Name: 'ACCELERATOR_REPOSITORY_SOURCE', + Type: 'PLAINTEXT', + Value: { + Ref: 'RepositorySource', + }, + }, + { + Name: 'ACCELERATOR_REPOSITORY_OWNER', + Type: 'PLAINTEXT', + Value: { + Ref: 'RepositoryOwner', + }, + }, + { + Name: 'ACCELERATOR_REPOSITORY_NAME', + Type: 'PLAINTEXT', + Value: { + Ref: 'RepositoryName', + }, + }, + { + Name: 'ACCELERATOR_REPOSITORY_BRANCH_NAME', + Type: 'PLAINTEXT', + Value: { + Ref: 'RepositoryBranchName', + }, + }, + { + Name: 'ACCELERATOR_ENABLE_APPROVAL_STAGE', + Type: 'PLAINTEXT', + Value: { + Ref: 'EnableApprovalStage', + }, + }, + { + Name: 'APPROVAL_STAGE_NOTIFY_EMAIL_LIST', + Type: 'PLAINTEXT', + Value: { + 'Fn::Join': [ + ',', + { + Ref: 'ApprovalStageNotifyEmailList', + }, + ], + }, + }, + { + Name: 'MANAGEMENT_ACCOUNT_EMAIL', + Type: 'PLAINTEXT', + Value: { + Ref: 'ManagementAccountEmail', + }, + }, + { + Name: 'LOG_ARCHIVE_ACCOUNT_EMAIL', + Type: 'PLAINTEXT', + Value: { + Ref: 'LogArchiveAccountEmail', + }, + }, + { + Name: 'AUDIT_ACCOUNT_EMAIL', + Type: 'PLAINTEXT', + Value: { + Ref: 'AuditAccountEmail', + }, + }, + { + Name: 'ENABLE_TESTER', + Type: 'PLAINTEXT', + Value: 'true', + }, + { + Name: 'MANAGEMENT_CROSS_ACCOUNT_ROLE_NAME', + Type: 'PLAINTEXT', + Value: 'AWSControlTowerExecution', + }, + ], + Image: 'aws/codebuild/standard:5.0', + ImagePullCredentialsType: 'CODEBUILD', + PrivilegedMode: true, + Type: 'LINUX_CONTAINER', + }, + ServiceRole: { + 'Fn::GetAtt': ['InstallerAdminRole7DEE4AC8', 'Arn'], + }, + Source: { + BuildSpec: { + 'Fn::Join': [ + '', + [ + 'version: "0.2"\nphases:\n install:\n runtime-versions:\n nodejs: 14\n pre_build:\n commands:\n - ENABLE_EXTERNAL_PIPELINE_ACCOUNT="no"\n - if [ ! -z "$MANAGEMENT_ACCOUNT_ID" ] && [ ! -z "$MANAGEMENT_ACCOUNT_ROLE_NAME" ]; then ENABLE_EXTERNAL_PIPELINE_ACCOUNT="yes"; fi\n build:\n commands:\n - cd source\n - yarn install\n - yarn lerna link\n - yarn build\n - cd packages/@aws-accelerator/installer\n - yarn run cdk bootstrap --toolkitStackName AWSAccelerator-CDKToolkit aws://', + { + Ref: 'AWS::AccountId', + }, + '/', + { + Ref: 'AWS::Region', + }, + ' --qualifier accel\n - yarn run cdk bootstrap --toolkitStackName AWSAccelerator-CDKToolkit aws://', + { + Ref: 'AWS::AccountId', + }, + '/', + { + 'Fn::FindInMap': [ + 'GlobalRegionMap', + { + Ref: 'AWS::Partition', + }, + 'regionName', + ], + }, + ' --qualifier accel\n - |-\n if [ $ENABLE_EXTERNAL_PIPELINE_ACCOUNT = "yes" ]; then\n export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s" $(aws sts assume-role --role-arn arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::"$MANAGEMENT_ACCOUNT_ID":role/"$MANAGEMENT_ACCOUNT_ROLE_NAME" --role-session-name acceleratorAssumeRoleSession --query "Credentials.[AccessKeyId,SecretAccessKey,SessionToken]" --output text));\n yarn run cdk bootstrap --toolkitStackName AWSAccelerator-CDKToolkit aws://$MANAGEMENT_ACCOUNT_ID/', + { + Ref: 'AWS::Region', + }, + ' --qualifier accel;\n yarn run cdk bootstrap --toolkitStackName AWSAccelerator-CDKToolkit aws://$MANAGEMENT_ACCOUNT_ID/', + { + 'Fn::FindInMap': [ + 'GlobalRegionMap', + { + Ref: 'AWS::Partition', + }, + 'regionName', + ], + }, + ' --qualifier accel;\n unset AWS_ACCESS_KEY_ID;\n unset AWS_SECRET_ACCESS_KEY;\n unset AWS_SESSION_TOKEN;\n fi\n - cd ../accelerator\n - yarn run ts-node --transpile-only cdk.ts deploy --require-approval never --stage pipeline --account ', + { + Ref: 'AWS::AccountId', + }, + ' --region ', + { + Ref: 'AWS::Region', + }, + ' --partition ', + { + Ref: 'AWS::Partition', + }, + '\n - if [ "$ENABLE_TESTER" = "true" ]; then yarn run ts-node --transpile-only cdk.ts deploy --require-approval never --stage tester-pipeline --account ', + { + Ref: 'AWS::AccountId', + }, + ' --region ', + { + Ref: 'AWS::Region', + }, + '; fi\n', + ], + ], + }, + Type: 'CODEPIPELINE', + }, + Cache: { + Type: 'NO_CACHE', + }, + EncryptionKey: { + 'Fn::GetAtt': ['InstallerKey2A6A8C6D', 'Arn'], + }, + Name: 'AWSAccelerator-InstallerProject', + }, + }, + }, + }); + }); + + /** + * External pipeline account stack with tester pipeline - Installer project resource test + */ + test(`${testNamePrefix} External pipeline account stack with tester pipeline - Installer project resource test`, () => { + cdk.assertions.Template.fromStack(externalPipelineAccountStackWithTesterPipeline).templateMatches({ + Resources: { + InstallerProject879FF821: { + Type: 'AWS::CodeBuild::Project', + Properties: { + Artifacts: { + Type: 'CODEPIPELINE', + }, + Environment: { + ComputeType: 'BUILD_GENERAL1_MEDIUM', + EnvironmentVariables: [ + { + Name: 'NODE_OPTIONS', + Type: 'PLAINTEXT', + Value: '--max_old_space_size=4096', + }, + { + Name: 'CDK_NEW_BOOTSTRAP', + Type: 'PLAINTEXT', + Value: '1', + }, + { + Name: 'ACCELERATOR_REPOSITORY_SOURCE', + Type: 'PLAINTEXT', + Value: { + Ref: 'RepositorySource', + }, + }, + { + Name: 'ACCELERATOR_REPOSITORY_OWNER', + Type: 'PLAINTEXT', + Value: { + Ref: 'RepositoryOwner', + }, + }, + { + Name: 'ACCELERATOR_REPOSITORY_NAME', + Type: 'PLAINTEXT', + Value: { + Ref: 'RepositoryName', + }, + }, + { + Name: 'ACCELERATOR_REPOSITORY_BRANCH_NAME', + Type: 'PLAINTEXT', + Value: { + Ref: 'RepositoryBranchName', + }, + }, + { + Name: 'ACCELERATOR_ENABLE_APPROVAL_STAGE', + Type: 'PLAINTEXT', + Value: { + Ref: 'EnableApprovalStage', + }, + }, + { + Name: 'APPROVAL_STAGE_NOTIFY_EMAIL_LIST', + Type: 'PLAINTEXT', + Value: { + 'Fn::Join': [ + ',', + { + Ref: 'ApprovalStageNotifyEmailList', + }, + ], + }, + }, + { + Name: 'MANAGEMENT_ACCOUNT_EMAIL', + Type: 'PLAINTEXT', + Value: { + Ref: 'ManagementAccountEmail', + }, + }, + { + Name: 'LOG_ARCHIVE_ACCOUNT_EMAIL', + Type: 'PLAINTEXT', + Value: { + Ref: 'LogArchiveAccountEmail', + }, + }, + { + Name: 'AUDIT_ACCOUNT_EMAIL', + Type: 'PLAINTEXT', + Value: { + Ref: 'AuditAccountEmail', + }, + }, + { + Name: 'MANAGEMENT_ACCOUNT_ID', + Type: 'PLAINTEXT', + Value: { + Ref: 'ManagementAccountId', + }, + }, + { + Name: 'MANAGEMENT_ACCOUNT_ROLE_NAME', + Type: 'PLAINTEXT', + Value: { + Ref: 'ManagementAccountRoleName', + }, + }, + { + Name: 'ACCELERATOR_QUALIFIER', + Type: 'PLAINTEXT', + Value: { + Ref: 'AcceleratorQualifier', + }, + }, + { + Name: 'ENABLE_TESTER', + Type: 'PLAINTEXT', + Value: 'true', + }, + { + Name: 'MANAGEMENT_CROSS_ACCOUNT_ROLE_NAME', + Type: 'PLAINTEXT', + Value: 'AWSControlTowerExecution', + }, + ], + Image: 'aws/codebuild/standard:5.0', + ImagePullCredentialsType: 'CODEBUILD', + PrivilegedMode: true, + Type: 'LINUX_CONTAINER', + }, + ServiceRole: { + 'Fn::GetAtt': ['InstallerAdminRole7DEE4AC8', 'Arn'], + }, + Source: { + BuildSpec: { + 'Fn::Join': [ + '', + [ + 'version: "0.2"\nphases:\n install:\n runtime-versions:\n nodejs: 14\n pre_build:\n commands:\n - ENABLE_EXTERNAL_PIPELINE_ACCOUNT="no"\n - if [ ! -z "$MANAGEMENT_ACCOUNT_ID" ] && [ ! -z "$MANAGEMENT_ACCOUNT_ROLE_NAME" ]; then ENABLE_EXTERNAL_PIPELINE_ACCOUNT="yes"; fi\n build:\n commands:\n - cd source\n - yarn install\n - yarn lerna link\n - yarn build\n - cd packages/@aws-accelerator/installer\n - yarn run cdk bootstrap --toolkitStackName AWSAccelerator-CDKToolkit aws://', + { + Ref: 'AWS::AccountId', + }, + '/', + { + Ref: 'AWS::Region', + }, + ' --qualifier accel\n - yarn run cdk bootstrap --toolkitStackName AWSAccelerator-CDKToolkit aws://', + { + Ref: 'AWS::AccountId', + }, + '/', + { + 'Fn::FindInMap': [ + 'GlobalRegionMap', + { + Ref: 'AWS::Partition', + }, + 'regionName', + ], + }, + ' --qualifier accel\n - |-\n if [ $ENABLE_EXTERNAL_PIPELINE_ACCOUNT = "yes" ]; then\n export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s" $(aws sts assume-role --role-arn arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::"$MANAGEMENT_ACCOUNT_ID":role/"$MANAGEMENT_ACCOUNT_ROLE_NAME" --role-session-name acceleratorAssumeRoleSession --query "Credentials.[AccessKeyId,SecretAccessKey,SessionToken]" --output text));\n yarn run cdk bootstrap --toolkitStackName AWSAccelerator-CDKToolkit aws://$MANAGEMENT_ACCOUNT_ID/', + { + Ref: 'AWS::Region', + }, + ' --qualifier accel;\n yarn run cdk bootstrap --toolkitStackName AWSAccelerator-CDKToolkit aws://$MANAGEMENT_ACCOUNT_ID/', + { + 'Fn::FindInMap': [ + 'GlobalRegionMap', + { + Ref: 'AWS::Partition', + }, + 'regionName', + ], + }, + ' --qualifier accel;\n unset AWS_ACCESS_KEY_ID;\n unset AWS_SECRET_ACCESS_KEY;\n unset AWS_SESSION_TOKEN;\n fi\n - cd ../accelerator\n - yarn run ts-node --transpile-only cdk.ts deploy --require-approval never --stage pipeline --account ', + { + Ref: 'AWS::AccountId', + }, + ' --region ', + { + Ref: 'AWS::Region', + }, + ' --partition ', + { + Ref: 'AWS::Partition', + }, + '\n - if [ "$ENABLE_TESTER" = "true" ]; then yarn run ts-node --transpile-only cdk.ts deploy --require-approval never --stage tester-pipeline --account ', + { + Ref: 'AWS::AccountId', + }, + ' --region ', + { + Ref: 'AWS::Region', + }, + '; fi\n', + ], + ], + }, + Type: 'CODEPIPELINE', + }, + Cache: { + Type: 'NO_CACHE', + }, + EncryptionKey: { + 'Fn::GetAtt': ['InstallerKey2A6A8C6D', 'Arn'], + }, + }, + }, + }, + }); + }); + + /** + * Management account pipeline stack without tester pipeline - Installer project resource test + */ + test(`${testNamePrefix} Management account pipeline stack without tester pipeline - Installer project resource test`, () => { + cdk.assertions.Template.fromStack(managementAccountStackWithoutTesterPipeline).templateMatches({ + Resources: { + InstallerProject879FF821: { + Type: 'AWS::CodeBuild::Project', + Properties: { + Artifacts: { + Type: 'CODEPIPELINE', + }, + EncryptionKey: { + 'Fn::GetAtt': ['InstallerKey2A6A8C6D', 'Arn'], + }, + Environment: { + ComputeType: 'BUILD_GENERAL1_MEDIUM', + EnvironmentVariables: [ + { + Name: 'NODE_OPTIONS', + Type: 'PLAINTEXT', + Value: '--max_old_space_size=4096', + }, + { + Name: 'CDK_NEW_BOOTSTRAP', + Type: 'PLAINTEXT', + Value: '1', + }, + { + Name: 'ACCELERATOR_REPOSITORY_SOURCE', + Type: 'PLAINTEXT', + Value: { + Ref: 'RepositorySource', + }, + }, + { + Name: 'ACCELERATOR_REPOSITORY_OWNER', + Type: 'PLAINTEXT', + Value: { + Ref: 'RepositoryOwner', + }, + }, + { + Name: 'ACCELERATOR_REPOSITORY_NAME', + Type: 'PLAINTEXT', + Value: { + Ref: 'RepositoryName', + }, + }, + { + Name: 'ACCELERATOR_REPOSITORY_BRANCH_NAME', + Type: 'PLAINTEXT', + Value: { + Ref: 'RepositoryBranchName', + }, + }, + { + Name: 'ACCELERATOR_ENABLE_APPROVAL_STAGE', + Type: 'PLAINTEXT', + Value: { + Ref: 'EnableApprovalStage', + }, + }, + { + Name: 'APPROVAL_STAGE_NOTIFY_EMAIL_LIST', + Type: 'PLAINTEXT', + Value: { + 'Fn::Join': [ + ',', + { + Ref: 'ApprovalStageNotifyEmailList', + }, + ], + }, + }, + { + Name: 'MANAGEMENT_ACCOUNT_EMAIL', + Type: 'PLAINTEXT', + Value: { + Ref: 'ManagementAccountEmail', + }, + }, + { + Name: 'LOG_ARCHIVE_ACCOUNT_EMAIL', + Type: 'PLAINTEXT', + Value: { + Ref: 'LogArchiveAccountEmail', + }, + }, + { + Name: 'AUDIT_ACCOUNT_EMAIL', + Type: 'PLAINTEXT', + Value: { + Ref: 'AuditAccountEmail', + }, + }, + ], + Image: 'aws/codebuild/standard:5.0', + ImagePullCredentialsType: 'CODEBUILD', + PrivilegedMode: true, + Type: 'LINUX_CONTAINER', + }, + Name: 'AWSAccelerator-InstallerProject', + ServiceRole: { + 'Fn::GetAtt': ['InstallerAdminRole7DEE4AC8', 'Arn'], + }, + Source: { + BuildSpec: { + 'Fn::Join': [ + '', + [ + 'version: "0.2"\nphases:\n install:\n runtime-versions:\n nodejs: 14\n pre_build:\n commands:\n - ENABLE_EXTERNAL_PIPELINE_ACCOUNT="no"\n - if [ ! -z "$MANAGEMENT_ACCOUNT_ID" ] && [ ! -z "$MANAGEMENT_ACCOUNT_ROLE_NAME" ]; then ENABLE_EXTERNAL_PIPELINE_ACCOUNT="yes"; fi\n build:\n commands:\n - cd source\n - yarn install\n - yarn lerna link\n - yarn build\n - cd packages/@aws-accelerator/installer\n - yarn run cdk bootstrap --toolkitStackName AWSAccelerator-CDKToolkit aws://', + { + Ref: 'AWS::AccountId', + }, + '/', + { + Ref: 'AWS::Region', + }, + ' --qualifier accel\n - yarn run cdk bootstrap --toolkitStackName AWSAccelerator-CDKToolkit aws://', + { + Ref: 'AWS::AccountId', + }, + '/', + { + 'Fn::FindInMap': [ + 'GlobalRegionMap', + { + Ref: 'AWS::Partition', + }, + 'regionName', + ], + }, + ' --qualifier accel\n - |-\n if [ $ENABLE_EXTERNAL_PIPELINE_ACCOUNT = "yes" ]; then\n export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s" $(aws sts assume-role --role-arn arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::"$MANAGEMENT_ACCOUNT_ID":role/"$MANAGEMENT_ACCOUNT_ROLE_NAME" --role-session-name acceleratorAssumeRoleSession --query "Credentials.[AccessKeyId,SecretAccessKey,SessionToken]" --output text));\n yarn run cdk bootstrap --toolkitStackName AWSAccelerator-CDKToolkit aws://$MANAGEMENT_ACCOUNT_ID/', + { + Ref: 'AWS::Region', + }, + ' --qualifier accel;\n yarn run cdk bootstrap --toolkitStackName AWSAccelerator-CDKToolkit aws://$MANAGEMENT_ACCOUNT_ID/', + { + 'Fn::FindInMap': [ + 'GlobalRegionMap', + { + Ref: 'AWS::Partition', + }, + 'regionName', + ], + }, + ' --qualifier accel;\n unset AWS_ACCESS_KEY_ID;\n unset AWS_SECRET_ACCESS_KEY;\n unset AWS_SESSION_TOKEN;\n fi\n - cd ../accelerator\n - yarn run ts-node --transpile-only cdk.ts deploy --require-approval never --stage pipeline --account ', + { + Ref: 'AWS::AccountId', + }, + ' --region ', + { + Ref: 'AWS::Region', + }, + ' --partition ', + { + Ref: 'AWS::Partition', + }, + '\n - if [ "$ENABLE_TESTER" = "true" ]; then yarn run ts-node --transpile-only cdk.ts deploy --require-approval never --stage tester-pipeline --account ', + { + Ref: 'AWS::AccountId', + }, + ' --region ', + { + Ref: 'AWS::Region', + }, + '; fi\n', + ], + ], + }, + Type: 'CODEPIPELINE', + }, + }, + }, + }, + }); + }); + + /** + * External pipeline account stack without tester pipeline - Installer project resource test + */ + test(`${testNamePrefix} External pipeline account stack without tester pipeline - Installer project resource test`, () => { + cdk.assertions.Template.fromStack(externalPipelineAccountStackWithoutTesterPipeline).templateMatches({ + Resources: { + InstallerProject879FF821: { + Type: 'AWS::CodeBuild::Project', + Properties: { + Artifacts: { + Type: 'CODEPIPELINE', + }, + Cache: { + Type: 'NO_CACHE', + }, + EncryptionKey: { + 'Fn::GetAtt': ['InstallerKey2A6A8C6D', 'Arn'], + }, + Environment: { + ComputeType: 'BUILD_GENERAL1_MEDIUM', + EnvironmentVariables: [ + { + Name: 'NODE_OPTIONS', + Type: 'PLAINTEXT', + Value: '--max_old_space_size=4096', + }, + { + Name: 'CDK_NEW_BOOTSTRAP', + Type: 'PLAINTEXT', + Value: '1', + }, + { + Name: 'ACCELERATOR_REPOSITORY_SOURCE', + Type: 'PLAINTEXT', + Value: { + Ref: 'RepositorySource', + }, + }, + { + Name: 'ACCELERATOR_REPOSITORY_OWNER', + Type: 'PLAINTEXT', + Value: { + Ref: 'RepositoryOwner', + }, + }, + { + Name: 'ACCELERATOR_REPOSITORY_NAME', + Type: 'PLAINTEXT', + Value: { + Ref: 'RepositoryName', + }, + }, + { + Name: 'ACCELERATOR_REPOSITORY_BRANCH_NAME', + Type: 'PLAINTEXT', + Value: { + Ref: 'RepositoryBranchName', + }, + }, + { + Name: 'ACCELERATOR_ENABLE_APPROVAL_STAGE', + Type: 'PLAINTEXT', + Value: { + Ref: 'EnableApprovalStage', + }, + }, + { + Name: 'APPROVAL_STAGE_NOTIFY_EMAIL_LIST', + Type: 'PLAINTEXT', + Value: { + 'Fn::Join': [ + ',', + { + Ref: 'ApprovalStageNotifyEmailList', + }, + ], + }, + }, + { + Name: 'MANAGEMENT_ACCOUNT_EMAIL', + Type: 'PLAINTEXT', + Value: { + Ref: 'ManagementAccountEmail', + }, + }, + { + Name: 'LOG_ARCHIVE_ACCOUNT_EMAIL', + Type: 'PLAINTEXT', + Value: { + Ref: 'LogArchiveAccountEmail', + }, + }, + { + Name: 'AUDIT_ACCOUNT_EMAIL', + Type: 'PLAINTEXT', + Value: { + Ref: 'AuditAccountEmail', + }, + }, + { + Name: 'MANAGEMENT_ACCOUNT_ID', + Type: 'PLAINTEXT', + Value: { + Ref: 'ManagementAccountId', + }, + }, + { + Name: 'MANAGEMENT_ACCOUNT_ROLE_NAME', + Type: 'PLAINTEXT', + Value: { + Ref: 'ManagementAccountRoleName', + }, + }, + { + Name: 'ACCELERATOR_QUALIFIER', + Type: 'PLAINTEXT', + Value: { + Ref: 'AcceleratorQualifier', + }, + }, + ], + Image: 'aws/codebuild/standard:5.0', + ImagePullCredentialsType: 'CODEBUILD', + PrivilegedMode: true, + Type: 'LINUX_CONTAINER', + }, + Name: { + 'Fn::Join': [ + '', + [ + { + Ref: 'AcceleratorQualifier', + }, + '-installer-project', + ], + ], + }, + ServiceRole: { + 'Fn::GetAtt': ['InstallerAdminRole7DEE4AC8', 'Arn'], + }, + Source: { + BuildSpec: { + 'Fn::Join': [ + '', + [ + 'version: "0.2"\nphases:\n install:\n runtime-versions:\n nodejs: 14\n pre_build:\n commands:\n - ENABLE_EXTERNAL_PIPELINE_ACCOUNT="no"\n - if [ ! -z "$MANAGEMENT_ACCOUNT_ID" ] && [ ! -z "$MANAGEMENT_ACCOUNT_ROLE_NAME" ]; then ENABLE_EXTERNAL_PIPELINE_ACCOUNT="yes"; fi\n build:\n commands:\n - cd source\n - yarn install\n - yarn lerna link\n - yarn build\n - cd packages/@aws-accelerator/installer\n - yarn run cdk bootstrap --toolkitStackName AWSAccelerator-CDKToolkit aws://', + { + Ref: 'AWS::AccountId', + }, + '/', + { + Ref: 'AWS::Region', + }, + ' --qualifier accel\n - yarn run cdk bootstrap --toolkitStackName AWSAccelerator-CDKToolkit aws://', + { + Ref: 'AWS::AccountId', + }, + '/', + { + 'Fn::FindInMap': [ + 'GlobalRegionMap', + { + Ref: 'AWS::Partition', + }, + 'regionName', + ], + }, + ' --qualifier accel\n - |-\n if [ $ENABLE_EXTERNAL_PIPELINE_ACCOUNT = "yes" ]; then\n export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s" $(aws sts assume-role --role-arn arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::"$MANAGEMENT_ACCOUNT_ID":role/"$MANAGEMENT_ACCOUNT_ROLE_NAME" --role-session-name acceleratorAssumeRoleSession --query "Credentials.[AccessKeyId,SecretAccessKey,SessionToken]" --output text));\n yarn run cdk bootstrap --toolkitStackName AWSAccelerator-CDKToolkit aws://$MANAGEMENT_ACCOUNT_ID/', + { + Ref: 'AWS::Region', + }, + ' --qualifier accel;\n yarn run cdk bootstrap --toolkitStackName AWSAccelerator-CDKToolkit aws://$MANAGEMENT_ACCOUNT_ID/', + { + 'Fn::FindInMap': [ + 'GlobalRegionMap', + { + Ref: 'AWS::Partition', + }, + 'regionName', + ], + }, + ' --qualifier accel;\n unset AWS_ACCESS_KEY_ID;\n unset AWS_SECRET_ACCESS_KEY;\n unset AWS_SESSION_TOKEN;\n fi\n - cd ../accelerator\n - yarn run ts-node --transpile-only cdk.ts deploy --require-approval never --stage pipeline --account ', + { + Ref: 'AWS::AccountId', + }, + ' --region ', + { + Ref: 'AWS::Region', + }, + ' --partition ', + { + Ref: 'AWS::Partition', + }, + '\n - if [ "$ENABLE_TESTER" = "true" ]; then yarn run ts-node --transpile-only cdk.ts deploy --require-approval never --stage tester-pipeline --account ', + { + Ref: 'AWS::AccountId', + }, + ' --region ', + { + Ref: 'AWS::Region', + }, + '; fi\n', + ], + ], + }, + Type: 'CODEPIPELINE', + }, + }, + }, + }, + }); + }); + + /** + * Management account pipeline stack - CodePipeline iam role default policy resource test + */ + test(`${testNamePrefix} Management account pipeline stack - CodePipeline iam role default policy resource test`, () => { + cdk.assertions.Template.fromStack(managementAccountStackWithTesterPipeline).templateMatches({ + Resources: { + CodeCommitPipelineRoleDefaultPolicyDE8B332B: { + Type: 'AWS::IAM::Policy', + Properties: { + PolicyDocument: { + Statement: [ + { + Action: [ + 's3:GetObject*', + 's3:GetBucket*', + 's3:List*', + 's3:DeleteObject*', + 's3:PutObject', + 's3:PutObjectLegalHold', + 's3:PutObjectRetention', + 's3:PutObjectTagging', + 's3:PutObjectVersionTagging', + 's3:Abort*', + ], + Effect: 'Allow', + Resource: [ + { + 'Fn::GetAtt': ['SecureBucket747CD8C0', 'Arn'], + }, + { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': ['SecureBucket747CD8C0', 'Arn'], + }, + '/*', + ], + ], + }, + ], + }, + { + Action: ['kms:Decrypt', 'kms:DescribeKey', 'kms:Encrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*'], + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': ['InstallerKey2A6A8C6D', 'Arn'], + }, + }, + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': ['CodeCommitPipelineSourceCodePipelineActionRoleFB176191', 'Arn'], + }, + }, + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': ['CodeCommitPipelineRole5C35E76C', 'Arn'], + }, + }, + { + Action: ['codebuild:BatchGetBuilds', 'codebuild:StartBuild', 'codebuild:StopBuild'], + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': ['InstallerProject879FF821', 'Arn'], + }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'CodeCommitPipelineRoleDefaultPolicyDE8B332B', + Roles: [ + { + Ref: 'CodeCommitPipelineRole5C35E76C', + }, + ], + }, + Condition: 'UseCodeCommitCondition', + }, + }, + }); + }); + + /** + * External pipeline account stack - CodePipeline iam role default policy resource test + */ + test(`${testNamePrefix} External pipeline account stack - CodePipeline iam role default policy resource test`, () => { + cdk.assertions.Template.fromStack(externalPipelineAccountStackWithTesterPipeline).templateMatches({ + Resources: { + CodeCommitPipelineRoleDefaultPolicyDE8B332B: { + Type: 'AWS::IAM::Policy', + Properties: { + PolicyDocument: { + Statement: [ + { + Action: [ + 's3:GetObject*', + 's3:GetBucket*', + 's3:List*', + 's3:DeleteObject*', + 's3:PutObject', + 's3:PutObjectLegalHold', + 's3:PutObjectRetention', + 's3:PutObjectTagging', + 's3:PutObjectVersionTagging', + 's3:Abort*', + ], + Effect: 'Allow', + Resource: [ + { + 'Fn::GetAtt': ['SecureBucket747CD8C0', 'Arn'], + }, + { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': ['SecureBucket747CD8C0', 'Arn'], + }, + '/*', + ], + ], + }, + ], + }, + { + Action: ['kms:Decrypt', 'kms:DescribeKey', 'kms:Encrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*'], + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': ['InstallerKey2A6A8C6D', 'Arn'], + }, + }, + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': ['CodeCommitPipelineSourceCodePipelineActionRoleFB176191', 'Arn'], + }, + }, + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': ['CodeCommitPipelineRole5C35E76C', 'Arn'], + }, + }, + { + Action: ['codebuild:BatchGetBuilds', 'codebuild:StartBuild', 'codebuild:StopBuild'], + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': ['InstallerProject879FF821', 'Arn'], + }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'CodeCommitPipelineRoleDefaultPolicyDE8B332B', + Roles: [ + { + Ref: 'CodeCommitPipelineRole5C35E76C', + }, + ], + }, + Condition: 'UseCodeCommitCondition', + }, + }, + }); + }); + + /** + * Management account pipeline stack - CodePipeline bucket KMS key resource test + */ + test(`${testNamePrefix} Management account pipeline stack - CodePipeline bucket KMS key resource test`, () => { + cdk.assertions.Template.fromStack(managementAccountStackWithTesterPipeline).templateMatches({ + Resources: { + InstallerKey2A6A8C6D: { + DeletionPolicy: 'Retain', + Properties: { + Description: 'AWS Accelerator Management Account Kms Key', + EnableKeyRotation: true, + KeyPolicy: { + Statement: [ + { + Effect: 'Allow', + Principal: { + AWS: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::', + { + Ref: 'AWS::AccountId', + }, + ':root', + ], + ], + }, + }, + Action: 'kms:*', + Resource: '*', + }, + { + Sid: 'Allow Accelerator Role to use the encryption key', + Effect: 'Allow', + Principal: { + AWS: '*', + }, + Action: ['kms:Encrypt', 'kms:Decrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*', 'kms:DescribeKey'], + Resource: '*', + Condition: { + ArnLike: { + 'aws:PrincipalARN': { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::', + { + Ref: 'AWS::AccountId', + }, + ':role/AWSAccelerator-*', + ], + ], + }, + }, + }, + }, + { + Sid: 'Allow SNS service to use the encryption key', + Effect: 'Allow', + Principal: { + Service: 'sns.amazonaws.com', + }, + Action: ['kms:Encrypt', 'kms:Decrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*', 'kms:DescribeKey'], + Resource: '*', + }, + { + 'Fn::If': [ + 'IsCommercialCondition', + { + Sid: 'KMS key access to codestar-notifications', + Effect: 'Allow', + Principal: { + Service: 'codestar-notifications.amazonaws.com', + }, + Action: ['kms:GenerateDataKey*', 'kms:Decrypt'], + Resource: '*', + Condition: { + StringEquals: { + 'kms:ViaService': { + 'Fn::Join': [ + '', + [ + 'sns.', + { + Ref: 'AWS::Region', + }, + '.amazonaws.com', + ], + ], + }, + }, + }, + }, + { + Ref: 'AWS::NoValue', + }, + ], + }, + ], + }, + }, + Type: 'AWS::KMS::Key', + UpdateReplacePolicy: 'Retain', + }, + }, + }); + }); + + /** + * External pipeline account stack - CodePipeline bucket KMS key resource test + */ + test(`${testNamePrefix} External pipeline account stack - CodePipeline bucket KMS key resource test`, () => { + cdk.assertions.Template.fromStack(externalPipelineAccountStackWithTesterPipeline).templateMatches({ + Resources: { + InstallerKey2A6A8C6D: { + Type: 'AWS::KMS::Key', + UpdateReplacePolicy: 'Retain', + DeletionPolicy: 'Retain', + Properties: { + Description: 'AWS Accelerator Management Account Kms Key', + EnableKeyRotation: true, + KeyPolicy: { + Statement: [ + { + Effect: 'Allow', + Principal: { + AWS: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::', + { + Ref: 'AWS::AccountId', + }, + ':root', + ], + ], + }, + }, + Action: 'kms:*', + Resource: '*', + }, + { + Sid: 'Allow Accelerator Role to use the encryption key', + Effect: 'Allow', + Principal: { + AWS: '*', + }, + Action: ['kms:Encrypt', 'kms:Decrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*', 'kms:DescribeKey'], + Resource: '*', + Condition: { + ArnLike: { + 'aws:PrincipalARN': { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::', + { + Ref: 'AWS::AccountId', + }, + ':role/', + { Ref: 'AcceleratorQualifier' }, + '-*', + ], + ], + }, + }, + }, + }, + { + Sid: 'Allow SNS service to use the encryption key', + Effect: 'Allow', + Principal: { + Service: 'sns.amazonaws.com', + }, + Action: ['kms:Encrypt', 'kms:Decrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*', 'kms:DescribeKey'], + Resource: '*', + }, + { + 'Fn::If': [ + 'IsCommercialCondition', + { + Sid: 'KMS key access to codestar-notifications', + Effect: 'Allow', + Principal: { + Service: 'codestar-notifications.amazonaws.com', + }, + Action: ['kms:GenerateDataKey*', 'kms:Decrypt'], + Resource: '*', + Condition: { + StringEquals: { + 'kms:ViaService': { + 'Fn::Join': [ + '', + [ + 'sns.', + { + Ref: 'AWS::Region', + }, + '.amazonaws.com', + ], + ], + }, + }, + }, + }, + { + Ref: 'AWS::NoValue', + }, + ], + }, + ], + }, + }, + }, + }, + }); + }); + + /** + * Management account pipeline stack - CodePipeline bucket policy resource test + */ + test(`${testNamePrefix} Management account pipeline stack - CodePipeline bucket policy resource test`, () => { + cdk.assertions.Template.fromStack(managementAccountStackWithTesterPipeline).templateMatches({ + Resources: { + SecureBucketPolicy6374AC61: { + Type: 'AWS::S3::BucketPolicy', + Properties: { + Bucket: { + Ref: 'SecureBucket747CD8C0', + }, + PolicyDocument: { + Statement: [ + { + Action: 's3:*', + Condition: { + Bool: { + 'aws:SecureTransport': 'false', + }, + }, + Effect: 'Deny', + Principal: { + AWS: '*', + }, + Resource: [ + { + 'Fn::GetAtt': ['SecureBucket747CD8C0', 'Arn'], + }, + { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': ['SecureBucket747CD8C0', 'Arn'], + }, + '/*', + ], + ], + }, + ], + Sid: 'deny-insecure-connections', + }, + ], + Version: '2012-10-17', + }, + }, + }, + }, + }); + }); + + /** + * Management account pipeline stack - CodePipeline bucket policy resource test + */ + test(`${testNamePrefix} Management account pipeline stack - CodePipeline bucket policy resource test`, () => { + cdk.assertions.Template.fromStack(managementAccountStackWithTesterPipeline).templateMatches({ + Resources: { + SecureBucketPolicy6374AC61: { + Type: 'AWS::S3::BucketPolicy', + Properties: { + Bucket: { + Ref: 'SecureBucket747CD8C0', + }, + PolicyDocument: { + Statement: [ + { + Action: 's3:*', + Condition: { + Bool: { + 'aws:SecureTransport': 'false', + }, + }, + Effect: 'Deny', + Principal: { + AWS: '*', + }, + Resource: [ + { + 'Fn::GetAtt': ['SecureBucket747CD8C0', 'Arn'], + }, + { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': ['SecureBucket747CD8C0', 'Arn'], + }, + '/*', + ], + ], + }, + ], + Sid: 'deny-insecure-connections', + }, + ], + Version: '2012-10-17', + }, + }, + }, + }, + }); + }); + + /** + * External pipeline account stack - CodePipeline bucket policy resource test + */ + test(`${testNamePrefix} External pipeline account stack - CodePipeline bucket policy resource test`, () => { + cdk.assertions.Template.fromStack(externalPipelineAccountStackWithTesterPipeline).templateMatches({ + Resources: { + SecureBucketPolicy6374AC61: { + Type: 'AWS::S3::BucketPolicy', + Properties: { + Bucket: { + Ref: 'SecureBucket747CD8C0', + }, + PolicyDocument: { + Statement: [ + { + Action: 's3:*', + Condition: { + Bool: { + 'aws:SecureTransport': 'false', + }, + }, + Effect: 'Deny', + Principal: { + AWS: '*', + }, + Resource: [ + { + 'Fn::GetAtt': ['SecureBucket747CD8C0', 'Arn'], + }, + { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': ['SecureBucket747CD8C0', 'Arn'], + }, + '/*', + ], + ], + }, + ], + Sid: 'deny-insecure-connections', + }, + ], + Version: '2012-10-17', + }, + }, + }, + }, + }); + }); + + /** + * Management account pipeline stack - CodePipeline bucket resource test + */ + test(`${testNamePrefix} Management account pipeline stack - CodePipeline bucket resource test`, () => { + cdk.assertions.Template.fromStack(managementAccountStackWithTesterPipeline).templateMatches({ + Resources: { + SecureBucket747CD8C0: { + Type: 'AWS::S3::Bucket', + UpdateReplacePolicy: 'Retain', + DeletionPolicy: 'Retain', + Properties: { + BucketEncryption: { + ServerSideEncryptionConfiguration: [ + { + ServerSideEncryptionByDefault: { + KMSMasterKeyID: { + 'Fn::GetAtt': ['InstallerKey2A6A8C6D', 'Arn'], + }, + SSEAlgorithm: 'aws:kms', + }, + }, + ], + }, + BucketName: { + 'Fn::Join': [ + '', + [ + 'aws-accelerator-installer-', + { + Ref: 'AWS::AccountId', + }, + '-', + { + Ref: 'AWS::Region', + }, + ], + ], + }, + OwnershipControls: { + Rules: [ + { + ObjectOwnership: 'BucketOwnerPreferred', + }, + ], + }, + PublicAccessBlockConfiguration: { + BlockPublicAcls: true, + BlockPublicPolicy: true, + IgnorePublicAcls: true, + RestrictPublicBuckets: true, + }, + VersioningConfiguration: { + Status: 'Enabled', + }, + }, + }, + }, + }); + }); + + /** + * External pipeline account stack - CodePipeline bucket resource test + */ + test(`${testNamePrefix} External pipeline account stack - CodePipeline bucket resource test`, () => { + cdk.assertions.Template.fromStack(externalPipelineAccountStackWithTesterPipeline).templateMatches({ + Resources: { + SecureBucket747CD8C0: { + Type: 'AWS::S3::Bucket', + UpdateReplacePolicy: 'Retain', + DeletionPolicy: 'Retain', + Properties: { + BucketEncryption: { + ServerSideEncryptionConfiguration: [ + { + ServerSideEncryptionByDefault: { + KMSMasterKeyID: { + 'Fn::GetAtt': ['InstallerKey2A6A8C6D', 'Arn'], + }, + SSEAlgorithm: 'aws:kms', + }, + }, + ], + }, + BucketName: { + 'Fn::Join': [ + '', + [ + { + Ref: 'AcceleratorQualifier', + }, + '-installer-', + { + Ref: 'AWS::AccountId', + }, + '-', + { + Ref: 'AWS::Region', + }, + ], + ], + }, + OwnershipControls: { + Rules: [ + { + ObjectOwnership: 'BucketOwnerPreferred', + }, + ], + }, + PublicAccessBlockConfiguration: { + BlockPublicAcls: true, + BlockPublicPolicy: true, + IgnorePublicAcls: true, + RestrictPublicBuckets: true, + }, + VersioningConfiguration: { + Status: 'Enabled', + }, + }, + }, + }, + }); + }); + + /** + * Management account pipeline stack - CodePipeline action iam role default policy resource test + */ + test(`${testNamePrefix} Management account pipeline stack - CodePipeline action iam role default policy resource test`, () => { + cdk.assertions.Template.fromStack(managementAccountStackWithTesterPipeline).templateMatches({ + Resources: { + CodeCommitPipelineSourceCodePipelineActionRoleDefaultPolicyF71E0C0D: { + Type: 'AWS::IAM::Policy', + Properties: { + PolicyDocument: { + Statement: [ + { + Action: [ + 's3:GetObject*', + 's3:GetBucket*', + 's3:List*', + 's3:DeleteObject*', + 's3:PutObject', + 's3:PutObjectLegalHold', + 's3:PutObjectRetention', + 's3:PutObjectTagging', + 's3:PutObjectVersionTagging', + 's3:Abort*', + ], + Effect: 'Allow', + Resource: [ + { + 'Fn::GetAtt': ['SecureBucket747CD8C0', 'Arn'], + }, + { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': ['SecureBucket747CD8C0', 'Arn'], + }, + '/*', + ], + ], + }, + ], + }, + { + Action: ['kms:Decrypt', 'kms:DescribeKey', 'kms:Encrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*'], + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': ['InstallerKey2A6A8C6D', 'Arn'], + }, + }, + { + Action: [ + 'codecommit:GetBranch', + 'codecommit:GetCommit', + 'codecommit:UploadArchive', + 'codecommit:GetUploadArchiveStatus', + 'codecommit:CancelUploadArchive', + ], + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':codecommit:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':', + { + Ref: 'RepositoryName', + }, + ], + ], + }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'CodeCommitPipelineSourceCodePipelineActionRoleDefaultPolicyF71E0C0D', + Roles: [ + { + Ref: 'CodeCommitPipelineSourceCodePipelineActionRoleFB176191', + }, + ], + }, + }, + }, + }); + }); + + /** + * External pipeline account stack - CodePipeline action iam role default policy resource test + */ + test(`${testNamePrefix} External pipeline account stack - CodePipeline action iam role default policy resource test`, () => { + cdk.assertions.Template.fromStack(externalPipelineAccountStackWithTesterPipeline).templateMatches({ + Resources: { + CodeCommitPipelineSourceCodePipelineActionRoleDefaultPolicyF71E0C0D: { + Type: 'AWS::IAM::Policy', + Properties: { + PolicyDocument: { + Statement: [ + { + Action: [ + 's3:GetObject*', + 's3:GetBucket*', + 's3:List*', + 's3:DeleteObject*', + 's3:PutObject', + 's3:PutObjectLegalHold', + 's3:PutObjectRetention', + 's3:PutObjectTagging', + 's3:PutObjectVersionTagging', + 's3:Abort*', + ], + Effect: 'Allow', + Resource: [ + { + 'Fn::GetAtt': ['SecureBucket747CD8C0', 'Arn'], + }, + { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': ['SecureBucket747CD8C0', 'Arn'], + }, + '/*', + ], + ], + }, + ], + }, + { + Action: ['kms:Decrypt', 'kms:DescribeKey', 'kms:Encrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*'], + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': ['InstallerKey2A6A8C6D', 'Arn'], + }, + }, + { + Action: [ + 'codecommit:GetBranch', + 'codecommit:GetCommit', + 'codecommit:UploadArchive', + 'codecommit:GetUploadArchiveStatus', + 'codecommit:CancelUploadArchive', + ], + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':codecommit:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':', + { + Ref: 'RepositoryName', + }, + ], + ], + }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'CodeCommitPipelineSourceCodePipelineActionRoleDefaultPolicyF71E0C0D', + Roles: [ + { + Ref: 'CodeCommitPipelineSourceCodePipelineActionRoleFB176191', + }, + ], + }, + }, + }, + }); + }); + + /** + * Management account pipeline stack - CodePipeline action iam role resource test + */ + test(`${testNamePrefix} Management account pipeline stack - CodePipeline action iam role resource test`, () => { + cdk.assertions.Template.fromStack(managementAccountStackWithTesterPipeline).templateMatches({ + Resources: { + CodeCommitPipelineSourceCodePipelineActionRoleFB176191: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + AWS: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::', + { + Ref: 'AWS::AccountId', + }, + ':root', + ], + ], + }, + }, + }, + ], + Version: '2012-10-17', + }, + }, + }, + }, + }); + }); + + /** + * External pipeline account stack - CodePipeline action iam role resource test + */ + test(`${testNamePrefix} External pipeline account stack - CodePipeline action iam role resource test`, () => { + cdk.assertions.Template.fromStack(externalPipelineAccountStackWithTesterPipeline).templateMatches({ + Resources: { + CodeCommitPipelineSourceCodePipelineActionRoleFB176191: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + AWS: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::', + { + Ref: 'AWS::AccountId', + }, + ':root', + ], + ], + }, + }, + }, + ], + Version: '2012-10-17', + }, + }, + }, + }, + }); + }); + + /** + * Management account pipeline stack - CodePipeline iam role resource test + */ + test(`${testNamePrefix} Management account pipeline stack - CodePipeline iam role resource test`, () => { + cdk.assertions.Template.fromStack(managementAccountStackWithTesterPipeline).templateMatches({ + Resources: { + CodeCommitPipelineRole5C35E76C: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'codepipeline.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + }, + }, + }, + }); + }); + + /** + * External pipeline account stack - CodePipeline iam role resource test + */ + test(`${testNamePrefix} External pipeline account stack - CodePipeline iam role resource test`, () => { + cdk.assertions.Template.fromStack(externalPipelineAccountStackWithTesterPipeline).templateMatches({ + Resources: { + CodeCommitPipelineRole5C35E76C: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'codepipeline.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + }, + }, + }, + }); + }); + + /** + * Management account pipeline stack - CodePipeline resource test + */ + test(`${testNamePrefix} Management account pipeline stack - CodePipeline resource test`, () => { + cdk.assertions.Template.fromStack(managementAccountStackWithTesterPipeline).templateMatches({ + Resources: { + CodeCommitPipeline2208527B: { + Type: 'AWS::CodePipeline::Pipeline', + DependsOn: ['CodeCommitPipelineRoleDefaultPolicyDE8B332B', 'CodeCommitPipelineRole5C35E76C'], + Properties: { + ArtifactStore: { + EncryptionKey: { + Id: { + 'Fn::GetAtt': ['InstallerKey2A6A8C6D', 'Arn'], + }, + Type: 'KMS', + }, + Location: { + Ref: 'SecureBucket747CD8C0', + }, + Type: 'S3', + }, + Name: 'AWSAccelerator-Installer', + RoleArn: { + 'Fn::GetAtt': ['CodeCommitPipelineRole5C35E76C', 'Arn'], + }, + Stages: [ + { + Actions: [ + { + ActionTypeId: { + Category: 'Source', + Owner: 'AWS', + Provider: 'CodeCommit', + Version: '1', + }, + Configuration: { + BranchName: { + Ref: 'RepositoryBranchName', + }, + PollForSourceChanges: false, + RepositoryName: { + Ref: 'RepositoryName', + }, + }, + Name: 'Source', + OutputArtifacts: [ + { + Name: 'Source', + }, + ], + RoleArn: { + 'Fn::GetAtt': ['CodeCommitPipelineSourceCodePipelineActionRoleFB176191', 'Arn'], + }, + RunOrder: 1, + }, + ], + Name: 'Source', + }, + { + Actions: [ + { + ActionTypeId: { + Category: 'Build', + Owner: 'AWS', + Provider: 'CodeBuild', + Version: '1', + }, + Configuration: { + ProjectName: { + Ref: 'InstallerProject879FF821', + }, + }, + InputArtifacts: [ + { + Name: 'Source', + }, + ], + Name: 'Install', + RoleArn: { + 'Fn::GetAtt': ['CodeCommitPipelineRole5C35E76C', 'Arn'], + }, + RunOrder: 1, + }, + ], + Name: 'Install', + }, + ], + }, + }, + }, + }); + }); + + /** + * External pipeline account stack - CodePipeline resource test + */ + test(`${testNamePrefix} External pipeline account stack - CodePipeline resource test`, () => { + cdk.assertions.Template.fromStack(externalPipelineAccountStackWithTesterPipeline).templateMatches({ + Resources: { + CodeCommitPipeline2208527B: { + Type: 'AWS::CodePipeline::Pipeline', + DependsOn: ['CodeCommitPipelineRoleDefaultPolicyDE8B332B', 'CodeCommitPipelineRole5C35E76C'], + Properties: { + ArtifactStore: { + EncryptionKey: { + Id: { + 'Fn::GetAtt': ['InstallerKey2A6A8C6D', 'Arn'], + }, + Type: 'KMS', + }, + Location: { + Ref: 'SecureBucket747CD8C0', + }, + Type: 'S3', + }, + Name: { + 'Fn::Join': [ + '', + [ + { + Ref: 'AcceleratorQualifier', + }, + '-installer', + ], + ], + }, + RestartExecutionOnUpdate: true, + RoleArn: { + 'Fn::GetAtt': ['CodeCommitPipelineRole5C35E76C', 'Arn'], + }, + Stages: [ + { + Actions: [ + { + ActionTypeId: { + Category: 'Source', + Owner: 'AWS', + Provider: 'CodeCommit', + Version: '1', + }, + Configuration: { + BranchName: { + Ref: 'RepositoryBranchName', + }, + PollForSourceChanges: false, + RepositoryName: { + Ref: 'RepositoryName', + }, + }, + Name: 'Source', + OutputArtifacts: [ + { + Name: 'Source', + }, + ], + RoleArn: { + 'Fn::GetAtt': ['CodeCommitPipelineSourceCodePipelineActionRoleFB176191', 'Arn'], + }, + RunOrder: 1, + }, + ], + Name: 'Source', + }, + { + Actions: [ + { + ActionTypeId: { + Category: 'Build', + Owner: 'AWS', + Provider: 'CodeBuild', + Version: '1', + }, + Configuration: { + ProjectName: { + Ref: 'InstallerProject879FF821', + }, + }, + InputArtifacts: [ + { + Name: 'Source', + }, + ], + Name: 'Install', + RoleArn: { + 'Fn::GetAtt': ['CodeCommitPipelineRole5C35E76C', 'Arn'], + }, + RunOrder: 1, + }, + ], + Name: 'Install', + }, + ], + }, + }, + }, + }); + }); + + /** + * External pipeline account SSM parameter SsmParamAcceleratorVersion resource test + */ + test(`${testNamePrefix} External pipeline account SSM parameter SsmParamAcceleratorVersion resource test`, () => { + cdk.assertions.Template.fromStack(externalPipelineAccountStackWithTesterPipeline).templateMatches({ + Resources: { + SsmParamAcceleratorVersionFF83282D: { + Type: 'AWS::SSM::Parameter', + Properties: { + Name: { + 'Fn::Join': [ + '', + [ + '/accelerator/', + { + Ref: 'AcceleratorQualifier', + }, + '/AWSAccelerator-Test-InstallerStack/version', + ], + ], + }, + Type: 'String', + }, + }, + }, + }); + }); + + /** + * External pipeline account SSM parameter SsmParamStackId resource test + */ + test(`${testNamePrefix} External pipeline account SSM parameter SsmParamStackId resource test`, () => { + cdk.assertions.Template.fromStack(externalPipelineAccountStackWithoutTesterPipeline).templateMatches({ + Resources: { + SsmParamStackId521A78D3: { + Type: 'AWS::SSM::Parameter', + Properties: { + Name: { + 'Fn::Join': [ + '', + [ + '/accelerator/', + { + Ref: 'AcceleratorQualifier', + }, + '/AWSAccelerator-Test-InstallerStack/stack-id', + ], + ], + }, + Type: 'String', + Value: { + Ref: 'AWS::StackId', + }, + }, + }, + }, + }); + }); + + /** + * External pipeline account SSM parameter SsmParamAcceleratorVersion resource test + */ + test(`${testNamePrefix} External pipeline account SSM parameter SsmParamAcceleratorVersion resource test`, () => { + cdk.assertions.Template.fromStack(managementAccountStackWithTesterPipeline).templateMatches({ + Resources: { + SsmParamAcceleratorVersionFF83282D: { + Type: 'AWS::SSM::Parameter', + Properties: { + Name: '/accelerator/AWSAccelerator-Test-InstallerStack/version', + Type: 'String', + }, + }, + }, + }); + }); + + /** + * Management account pipeline account SSM parameter SsmParamStackId resource test + */ + test(`${testNamePrefix} External pipeline account SSM parameter SsmParamStackId resource test`, () => { + cdk.assertions.Template.fromStack(managementAccountStackWithoutTesterPipeline).templateMatches({ + Resources: { + SsmParamStackId521A78D3: { + Type: 'AWS::SSM::Parameter', + Properties: { + Name: '/accelerator/AWSAccelerator-Test-InstallerStack/stack-id', + Type: 'String', + Value: { + Ref: 'AWS::StackId', + }, + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/installer/tsconfig.json b/source/packages/@aws-accelerator/installer/tsconfig.json new file mode 100644 index 000000000..d2979ce92 --- /dev/null +++ b/source/packages/@aws-accelerator/installer/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["lib/**/*", "bin/**/*", "index.ts"], + "exclude": ["cdk.out/**/*", "test/**/*"] +} diff --git a/source/packages/@aws-accelerator/tester/.npmignore b/source/packages/@aws-accelerator/tester/.npmignore new file mode 100644 index 000000000..c1d6d45dc --- /dev/null +++ b/source/packages/@aws-accelerator/tester/.npmignore @@ -0,0 +1,6 @@ +*.ts +!*.d.ts + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/source/packages/@aws-accelerator/tester/README.md b/source/packages/@aws-accelerator/tester/README.md new file mode 100644 index 000000000..ee3a2ad02 --- /dev/null +++ b/source/packages/@aws-accelerator/tester/README.md @@ -0,0 +1 @@ +# @aws-accelerator/tester diff --git a/source/packages/@aws-accelerator/tester/bin/app.ts b/source/packages/@aws-accelerator/tester/bin/app.ts new file mode 100644 index 000000000..fa5207b6c --- /dev/null +++ b/source/packages/@aws-accelerator/tester/bin/app.ts @@ -0,0 +1,79 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +import * as cdk from 'aws-cdk-lib'; +import * as yaml from 'js-yaml'; +import fs from 'fs'; +import path from 'path'; +import { TesterStack, CONFIG_FILE_NAME, CONFIG_FILE_CONTENT_TYPE } from '../lib/tester-stack'; + +/** + * Test Accelerator CDK App + */ +async function main() { + const usage = + 'Usage: app.ts --context account=ACCOUNT --context region=REGION --context management-cross-account-role-name=MANAGEMENT_CROSS_ACCOUNT_ROLE_NAME --context config-dir=CONFIG_DIRECTORY [--context qualifier=QUALIFIER] [--context management-account-id=MANAGEMENT_ACCOUNT_ID] [--context management-account-role-name=MANAGEMENT_ACCOUNT_ROLE_NAME]'; + const app = new cdk.App(); + + const account = app.node.tryGetContext('account'); + const region = app.node.tryGetContext('region'); + const qualifier = app.node.tryGetContext('qualifier'); + const managementCrossAccountRoleName = app.node.tryGetContext('management-cross-account-role-name'); + const configDirPath = app.node.tryGetContext('config-dir'); + + if (account === undefined) { + console.warn(`[tester-app] Invalid --account ${account}`); + throw new Error(usage); + } + + if (region === undefined) { + console.warn(`[tester-app] Invalid --account ${region}`); + throw new Error(usage); + } + + if (managementCrossAccountRoleName === undefined) { + console.warn(`[tester-app] Invalid --management-cross-account-role-name ${managementCrossAccountRoleName}`); + throw new Error(usage); + } + + if (configDirPath === undefined || !fs.existsSync(configDirPath)) { + console.warn(`[tester-app] Invalid --config-dir ${configDirPath}`); + throw new Error(usage); + } + + const configFilePath = path.join(configDirPath, CONFIG_FILE_NAME); + if (!fs.existsSync(configFilePath)) { + throw new Error(`[tester-app] Config file not found ${configFilePath}`); + } + + const configFileContent = yaml.load(fs.readFileSync(configFilePath, 'utf8')) as CONFIG_FILE_CONTENT_TYPE; + + new TesterStack( + app, + qualifier === undefined + ? `AWSAccelerator-TesterStack-${account}-${region}` + : `${qualifier}-tester-stack-${account}-${region}`, + { + synthesizer: new cdk.DefaultStackSynthesizer({ + generateBootstrapVersionRule: false, + }), + managementCrossAccountRoleName: managementCrossAccountRoleName, + configFileContent: configFileContent, + qualifier: qualifier === undefined ? 'aws-accelerator' : qualifier, + managementAccountId: app.node.tryGetContext('management-account-id'), + managementAccountRoleName: app.node.tryGetContext('management-account-role-name'), + }, + ); +} + +//call the main function +main(); diff --git a/source/packages/@aws-accelerator/tester/cdk.json b/source/packages/@aws-accelerator/tester/cdk.json new file mode 100644 index 000000000..41d719a0e --- /dev/null +++ b/source/packages/@aws-accelerator/tester/cdk.json @@ -0,0 +1,7 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/app.ts", + "versionReporting": false, + "context": { + "@aws-cdk/core:bootstrapQualifier": "accel" + } +} diff --git a/source/packages/@aws-accelerator/tester/index.ts b/source/packages/@aws-accelerator/tester/index.ts new file mode 100644 index 000000000..7d9b8288e --- /dev/null +++ b/source/packages/@aws-accelerator/tester/index.ts @@ -0,0 +1,14 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +export * from './lib/tester-stack'; diff --git a/source/packages/@aws-accelerator/tester/jest.config.js b/source/packages/@aws-accelerator/tester/jest.config.js new file mode 100644 index 000000000..0e8a6842d --- /dev/null +++ b/source/packages/@aws-accelerator/tester/jest.config.js @@ -0,0 +1,6 @@ +const base = require('../../../jest.config.base'); +const packageJson = require('./package.json'); + +module.exports = { + ...base.getJestJunitConfig(packageJson.name), +}; diff --git a/source/packages/@aws-accelerator/tester/lambdas/index.ts b/source/packages/@aws-accelerator/tester/lambdas/index.ts new file mode 100644 index 000000000..b9347d5ef --- /dev/null +++ b/source/packages/@aws-accelerator/tester/lambdas/index.ts @@ -0,0 +1,118 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { throttlingBackOff } from '@aws-accelerator/utils'; +import * as AWS from 'aws-sdk'; +import { validateTransitGateway } from './test-target-functions/validate-transit-gateway'; +AWS.config.logger = console; + +/** + * AWS Config custom config lambda handler + * + * @param event + * @returns + */ +export async function handler(event): Promise<{ + Status: string | undefined; + StatusCode: number | undefined; +}> { + console.log(event); + const resultToken = event['resultToken']; + + const ruleParameters = JSON.parse(event['ruleParameters']); + + const configRegion = ruleParameters['awsConfigRegion']; + const test = ruleParameters['test']; + + const managementCrossAccountRoleName = ruleParameters['managementAccount']['crossAccountRoleName']; + const partition = ruleParameters['managementAccount']['partition']; + const managementAccountId = ruleParameters['managementAccount']['id']; + const managementAccountRoleName = ruleParameters['managementAccount']['roleName']; + + const invokingEvent = JSON.parse(event['invokingEvent']); + const invokingAwsAccountId = invokingEvent['awsAccountId']; + + const stsClient = new AWS.STS({}); + let managementAccountCredential: AWS.STS.Credentials; + + // Create management account credential when invoking account is not management account + if (invokingAwsAccountId !== managementAccountId) { + const roleArn = `arn:${partition}:iam::${managementAccountId}:role/${managementAccountRoleName}`; + const response = await throttlingBackOff(() => + stsClient.assumeRole({ RoleArn: roleArn, RoleSessionName: 'acceleratorAssumeRoleSession' }).promise(), + ); + managementAccountCredential = response.Credentials!; + } else { + const credentials = stsClient.config.credentials as AWS.Credentials; + managementAccountCredential = { + AccessKeyId: credentials!.accessKeyId, + SecretAccessKey: credentials!.secretAccessKey, + SessionToken: credentials!.sessionToken!, + Expiration: credentials!.expireTime, + }; + } + + let response; + + if (test['suite'] === 'network') { + if (test['testTarget'] === 'validateTransitGateway') { + response = await validateTransitGateway( + configRegion, + { + partition: partition, + id: managementAccountId, + crossAccountRoleName: managementCrossAccountRoleName, + credential: managementAccountCredential, + }, + test['parameters'], + ); + } + } + + if (response) { + await putEvaluations(configRegion, resultToken, response); + } + return { Status: 'Success', StatusCode: 200 }; +} + +/** + * Function to config custom rule put evaluation + * @param configRegion + * @param resultToken + * @param result + */ +async function putEvaluations( + // configServiceClient: ConfigServiceClient, + configRegion: string, + resultToken: string, + result: { complianceResourceType: string; complianceResourceId: string; complianceType: string }, +): Promise { + //Put Evaluation + const configServiceClient = new AWS.ConfigService({ region: configRegion }); + await throttlingBackOff(() => + configServiceClient + .putEvaluations({ + Evaluations: [ + { + Annotation: 'Verified by custom lambda function', + ComplianceResourceId: result.complianceResourceId, + ComplianceResourceType: result.complianceResourceType, + ComplianceType: result.complianceType, + OrderingTimestamp: new Date(), + }, + ], + ResultToken: resultToken, + }) + .promise(), + ); +} diff --git a/source/packages/@aws-accelerator/tester/lambdas/package.json b/source/packages/@aws-accelerator/tester/lambdas/package.json new file mode 100644 index 000000000..c34f5ef74 --- /dev/null +++ b/source/packages/@aws-accelerator/tester/lambdas/package.json @@ -0,0 +1,46 @@ +{ + "name": "@aws-accelerator/tester-lambdas", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build --clean", + "build": "esbuild --minify --bundle --sourcemap --platform=node --target=node14 --external:aws-sdk --outfile=./dist/index.js index.ts", + "test": "", + "testreport": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \\\\\\\"*.d.ts\\\\\\\" ", + "precommit": "eslint --max-warnings 0 -c ../../../../.eslintrc.json '**/*.{ts,tsx}' --ignore-pattern \\\\\\\"*.d.ts\\\\\\\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-node": "10.4.0", + "ts-jest": "27.1.1", + "typescript": "4.5.2" + }, + "peerDependencies": { + "eslint": "8.4.1" + }, + "dependencies": { + "@aws-accelerator/utils": "^0.0.0", + "aws-sdk": "2.1001.0" + } +} diff --git a/source/packages/@aws-accelerator/tester/lambdas/test-target-functions/validate-transit-gateway.ts b/source/packages/@aws-accelerator/tester/lambdas/test-target-functions/validate-transit-gateway.ts new file mode 100644 index 000000000..f3104ade2 --- /dev/null +++ b/source/packages/@aws-accelerator/tester/lambdas/test-target-functions/validate-transit-gateway.ts @@ -0,0 +1,277 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { throttlingBackOff } from '@aws-accelerator/utils'; +import * as AWS from 'aws-sdk'; +AWS.config.logger = console; + +/** + * Config rule compliance value enum + */ +const enum ComplianceType { + Compliant = 'COMPLIANT', + Non_Compliant = 'NON_COMPLIANT', +} + +/** + * "validateTransitGateway" test target. Function to validate Transit Gateway. + * Validates following: + *
    + *
  • Transit gateway exists + *
  • Transit gateway has valid route tables + *
  • Transit gateway attachment accounts are valid + *
+ * @param configRegion {string} + * @param managementAccount {Object} + * @param parameters {string} + */ +export async function validateTransitGateway( + configRegion: string, + managementAccount: { partition: string; id: string; crossAccountRoleName: string; credential: AWS.STS.Credentials }, + parameters: string, +): Promise<{ complianceResourceType: string; complianceResourceId: string; complianceType: string }> { + const transitGatewayName = parameters['name']; + const transitGatewayAccountId = parameters['accountId']; + const transitGatewayRegion = parameters['region']; + const amazonSideAsn = parameters['amazonSideAsn']; + const dnsSupport = parameters['dnsSupport']; + const vpnEcmpSupport = parameters['vpnEcmpSupport']; + const autoAcceptSharingAttachments = parameters['autoAcceptSharingAttachments']; + const defaultRouteTableAssociation = parameters['defaultRouteTableAssociation']; + const defaultRouteTablePropagation = parameters['defaultRouteTablePropagation']; + const routeTableNames = parameters['routeTableNames']; + const shareTargetAccountIds = parameters['shareTargetAccountIds']; + + let ec2Client: AWS.EC2; + + // Assume role when transit gateway to be evaluated in other account than config account + if (managementAccount.id !== transitGatewayAccountId) { + const roleArn = `arn:${managementAccount.partition}:iam::${transitGatewayAccountId}:role/${managementAccount.crossAccountRoleName}`; + const stsClient = new AWS.STS({ + region: configRegion, + credentials: { + accessKeyId: managementAccount.credential.AccessKeyId!, + secretAccessKey: managementAccount.credential.SecretAccessKey!, + sessionToken: managementAccount.credential.SessionToken, + expireTime: managementAccount.credential.Expiration, + }, + }); + + const response = await throttlingBackOff(() => + stsClient.assumeRole({ RoleArn: roleArn, RoleSessionName: 'acceleratorAssumeRoleSession' }).promise(), + ); + + ec2Client = new AWS.EC2({ + region: transitGatewayRegion, + credentials: { + accessKeyId: response.Credentials!.AccessKeyId!, + secretAccessKey: response.Credentials!.SecretAccessKey!, + sessionToken: response.Credentials!.SessionToken, + expireTime: response.Credentials!.Expiration, + }, + }); + } else { + ec2Client = new AWS.EC2({ + region: transitGatewayRegion, + credentials: { + accessKeyId: managementAccount.credential.AccessKeyId!, + secretAccessKey: managementAccount.credential.SecretAccessKey!, + sessionToken: managementAccount.credential.SessionToken, + expireTime: managementAccount.credential.Expiration, + }, + }); + } + + const complianceResourceType = 'AWS::EC2::TransitGateway'; + let complianceResourceId = transitGatewayAccountId; + let complianceType = ComplianceType.Non_Compliant; + + let nextToken: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => ec2Client.describeTransitGateways({ NextToken: nextToken }).promise()); + for (const transitGateway of page.TransitGateways ?? []) { + if ( + await transitGatewayExists( + transitGateway, + transitGatewayName, + parseInt(amazonSideAsn ?? 0), + dnsSupport, + vpnEcmpSupport, + autoAcceptSharingAttachments, + defaultRouteTableAssociation, + defaultRouteTablePropagation, + ) + ) { + complianceResourceId = transitGateway.TransitGatewayId!; + if ( + (await isRouteTablesValid( + ec2Client, + transitGateway.TransitGatewayId!, + defaultRouteTableAssociation, + defaultRouteTablePropagation, + routeTableNames ?? [], + )) && + (await isTransitGatewayAttachmentsValid( + ec2Client, + transitGateway.TransitGatewayId!, + shareTargetAccountIds ?? [], + )) + ) { + // set compliance + complianceType = ComplianceType.Compliant; + } + } + } + nextToken = page.NextToken; + } while (nextToken); + return { + complianceResourceType: complianceResourceType, + complianceResourceId: complianceResourceId, + complianceType: complianceType, + }; +} + +/** + * Function to check if two arrays are exactly same with number of item and the position + * @param first + * @param second + */ +function areArraysEqual(first: string[], second: string[]) { + return ( + Array.isArray(first) && + Array.isArray(second) && + first.length === second.length && + first.every((val, index) => val === second[index]) + ); +} + +/** + * Function to check if transit gateway exists + * @param transitGateway + * @param validatingTransitGatewayName + * @param amazonSideAsn + * @param dnsSupport + * @param vpnEcmpSupport + * @param autoAcceptSharedAttachments + * @param defaultRouteTableAssociation + * @param defaultRouteTablePropagation + */ +async function transitGatewayExists( + transitGateway: AWS.EC2.TransitGateway, + validatingTransitGatewayName: string, + amazonSideAsn: number, + dnsSupport: string | undefined, + vpnEcmpSupport: string | undefined, + autoAcceptSharedAttachments: string | undefined, + defaultRouteTableAssociation: string | undefined, + defaultRouteTablePropagation: string | undefined, +): Promise { + if ( + transitGateway.Options!.AmazonSideAsn === + (amazonSideAsn !== 0 ? amazonSideAsn : transitGateway.Options!.AmazonSideAsn) && + transitGateway.Options!.DnsSupport === (dnsSupport ?? transitGateway.Options!.DnsSupport) && + transitGateway.Options!.VpnEcmpSupport === (vpnEcmpSupport ?? transitGateway.Options!.VpnEcmpSupport) && + transitGateway.Options!.AutoAcceptSharedAttachments === + (autoAcceptSharedAttachments ?? transitGateway.Options!.AutoAcceptSharedAttachments) && + transitGateway.Options!.DefaultRouteTableAssociation === + (defaultRouteTableAssociation ?? transitGateway.Options!.DefaultRouteTableAssociation) && + transitGateway.Options!.DefaultRouteTablePropagation === + (defaultRouteTablePropagation ?? transitGateway.Options!.DefaultRouteTablePropagation) + ) { + for (const tag of transitGateway.Tags ?? []) { + if (tag.Key === 'Name' && tag.Value === validatingTransitGatewayName) { + return true; + } + } + } + return false; +} + +/** + * Function to validate transit gateway route tables + * @param ec2Client + * @param transitGatewayId + * @param defaultRouteTableAssociation + * @param defaultRouteTablePropagation + * @param validatingRouteTableNames + */ +async function isRouteTablesValid( + ec2Client: AWS.EC2, + transitGatewayId: string, + defaultRouteTableAssociation: string | undefined, + defaultRouteTablePropagation: string | undefined, + validatingRouteTableNames: string[], +): Promise { + const presentRouteTableNames: string[] = []; + + let nextToken: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => + ec2Client.describeTransitGatewayRouteTables({ NextToken: nextToken }).promise(), + ); + for (const transitGatewayRouteTable of page.TransitGatewayRouteTables ?? []) { + if ( + transitGatewayRouteTable.TransitGatewayId === transitGatewayId && + transitGatewayRouteTable.State === 'available' && + transitGatewayRouteTable.DefaultAssociationRouteTable === + ((defaultRouteTableAssociation ?? transitGatewayRouteTable.DefaultAssociationRouteTable) === 'enable') && + transitGatewayRouteTable.DefaultPropagationRouteTable === + ((defaultRouteTablePropagation ?? transitGatewayRouteTable.DefaultPropagationRouteTable) === 'enable') + ) { + for (const tag of transitGatewayRouteTable.Tags ?? []) { + if (tag.Key === 'Name') { + presentRouteTableNames.push(tag.Value!); + } + } + } + } + nextToken = page.NextToken; + } while (nextToken); + + return validatingRouteTableNames.length === 0 + ? true + : areArraysEqual(validatingRouteTableNames.sort(), presentRouteTableNames.sort()); +} + +/** + * Function to validate transit gateway attachments + * @param ec2Client + * @param transitGatewayId + * @param shareTargetAccountIds + */ +async function isTransitGatewayAttachmentsValid( + ec2Client: AWS.EC2, + transitGatewayId: string, + shareTargetAccountIds: string[], +): Promise { + const resourceOwnerIds: string[] = []; + + let nextToken: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => + ec2Client.describeTransitGatewayAttachments({ NextToken: nextToken }).promise(), + ); + for (const transitGatewayAttachment of page.TransitGatewayAttachments ?? []) { + if ( + transitGatewayAttachment.TransitGatewayId === transitGatewayId && + transitGatewayAttachment.State === 'available' + ) { + resourceOwnerIds.push(transitGatewayAttachment.ResourceOwnerId!); + } + } + nextToken = page.NextToken; + } while (nextToken); + return shareTargetAccountIds.length === 0 + ? true + : areArraysEqual(shareTargetAccountIds.sort(), resourceOwnerIds.sort()); +} diff --git a/source/packages/@aws-accelerator/tester/lambdas/tsconfig.json b/source/packages/@aws-accelerator/tester/lambdas/tsconfig.json new file mode 100644 index 000000000..5fafbed5f --- /dev/null +++ b/source/packages/@aws-accelerator/tester/lambdas/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "noImplicitAny": false + }, + "include": [ + "index.ts" + ], + "exclude": ["test/**/*"] +} diff --git a/source/packages/@aws-accelerator/tester/lib/tester-stack.ts b/source/packages/@aws-accelerator/tester/lib/tester-stack.ts new file mode 100644 index 000000000..e1e644926 --- /dev/null +++ b/source/packages/@aws-accelerator/tester/lib/tester-stack.ts @@ -0,0 +1,149 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as config from 'aws-cdk-lib/aws-config'; +import path from 'path'; + +/** + * Test config file name + */ +export const CONFIG_FILE_NAME = 'config.yaml'; + +/** + * Test config structure type + */ +export type CONFIG_FILE_CONTENT_TYPE = { + /** + * List of test cases + */ + tests: [ + { + /** + * Unique test name + */ + name: string; + /** + * Test case description + */ + description: string; + /** + * Test suite + */ + suite: string; + /** + * Test target identifier + */ + testTarget: string; + /** + * Expected test result - PASS/FAIL + */ + expect: string; + /** + * List of test case input parameters + */ + parameters: Record[]; + }, + ]; +}; + +/** + * TesterStackPops + */ +export interface TesterStackPops extends cdk.StackProps { + readonly qualifier: string; + readonly configFileContent: CONFIG_FILE_CONTENT_TYPE; + readonly managementCrossAccountRoleName: string; + readonly managementAccountId?: string; + readonly managementAccountRoleName?: string; +} + +/** + * TesterStack class + */ +export class TesterStack extends cdk.Stack { + constructor(scope: Construct, id: string, props: TesterStackPops) { + super(scope, id, props); + + /** + * Custom policy statements + */ + const policyStatements: iam.PolicyStatement[] = []; + + if (props.managementAccountId && props.managementAccountRoleName) { + policyStatements.push( + new iam.PolicyStatement({ + sid: 'LambdaSTSActions', + effect: iam.Effect.ALLOW, + actions: ['sts:AssumeRole'], + resources: [ + `arn:${cdk.Stack.of(this).partition}:iam::${props.managementAccountId}:role/${ + props.managementAccountRoleName + }`, + ], + }), + ); + } else { + policyStatements.push( + new iam.PolicyStatement({ + sid: 'LambdaSTSActions', + effect: iam.Effect.ALLOW, + actions: ['sts:AssumeRole'], + resources: [`arn:${cdk.Stack.of(this).partition}:iam::*:role/${props.managementCrossAccountRoleName}`], + }), + ); + } + + for (const test of props.configFileContent.tests) { + const testName = test.name.replace(/[^a-zA-Z0-9-]/g, '-'); + + /** + * Lambda function for config custom role + * Single lambda function can not be used for multiple config custom role, there is a pending issue with CDK team on this + * https://github.com/aws/aws-cdk/issues/17582 + */ + const lambdaFunction = new lambda.Function(this, `${props.qualifier}-${testName}Function`, { + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler', + code: lambda.Code.fromAsset(path.join(__dirname, '../lambdas/dist')), + description: `AWS Config custom rule function used for test case "${test.name}"`, + }); + + lambdaFunction.role?.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('ReadOnlyAccess')); + + policyStatements.forEach(policyStatement => { + lambdaFunction?.addToRolePolicy(policyStatement); + }); + + new config.CustomRule(this, `${props.qualifier}-${testName}CustomRule`, { + configRuleName: `${props.qualifier}-${testName}`, + lambdaFunction: lambdaFunction, + periodic: true, + inputParameters: { + ['awsConfigRegion']: cdk.Stack.of(this).region, + ['managementAccount']: { + partition: cdk.Stack.of(this).partition, + id: props.managementAccountId ?? cdk.Stack.of(this).account, + crossAccountRoleName: props.managementCrossAccountRoleName, + roleName: props.managementAccountRoleName, + }, + ['test']: test, + }, + description: `${test.description}`, + maximumExecutionFrequency: config.MaximumExecutionFrequency.SIX_HOURS, // default is 24 hours + }); + } + } +} diff --git a/source/packages/@aws-accelerator/tester/package.json b/source/packages/@aws-accelerator/tester/package.json new file mode 100644 index 000000000..0cbabbd8b --- /dev/null +++ b/source/packages/@aws-accelerator/tester/package.json @@ -0,0 +1,55 @@ +{ + "name": "@aws-accelerator/tester", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "private": true, + "scripts": { + "cleanup": "tsc --build ../tests --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ../tests --clean", + "build": "tsc", + "watch": "tsc -w", + "test": "jest --coverage --ci", + "lint": "eslint --fix --max-warnings 0 -c ../../../.eslintrc.json 'lib/**/*.{ts,tsx}' --ignore-pattern \\\\\\\"*.d.ts\\\\\\\" ", + "precommit": "eslint --max-warnings 0 -c ../../../.eslintrc.json 'lib/**/*.{ts,tsx}' --ignore-pattern \\\\\\\"*.d.ts\\\\\\\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "@typescript-eslint/eslint-plugin": "5.6.0", + "@typescript-eslint/parser": "5.6.0", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "fs-extra": "10.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "ts-node": "10.4.0", + "typescript": "4.5.2", + "aws-cdk": "2.16.0", + "aws-cdk-lib": "2.16.0", + "constructs": "10.0.12" + }, + "peerDependencies": { + "aws-cdk-lib": "2.16.0", + "es-lint": "8.4.1" + }, + "dependencies": { + "@types/fs-extra": "9.0.13", + "aws-cdk": "2.16.0", + "aws-cdk-lib": "2.16.0", + "ts-node": "10.4.0" + } +} diff --git a/source/packages/@aws-accelerator/tester/test/configs/config.yaml b/source/packages/@aws-accelerator/tester/test/configs/config.yaml new file mode 100644 index 000000000..a7bc21a3e --- /dev/null +++ b/source/packages/@aws-accelerator/tester/test/configs/config.yaml @@ -0,0 +1,22 @@ +tests: + - name: validate main transit gateway + description: Validate Main Transit Gateway + suite: network + testTarget: validateTransitGateway + expect: PASS + parameters: + name: Main + accountId: '333333333333' + region: us-east-1 + amazonSideAsn: '65521' + dnsSupport: enable + vpnEcmpSupport: enable + defaultRouteTableAssociation: disable + defaultRouteTablePropagation: disable + autoAcceptSharingAttachments: enable + routeTableNames: + - core + - segregated + - shared + - standalone + shareTargetAccountIds: ['111111111111','222222222222'] \ No newline at end of file diff --git a/source/packages/@aws-accelerator/tester/test/external-pipeline-account-tester-stack.test.ts b/source/packages/@aws-accelerator/tester/test/external-pipeline-account-tester-stack.test.ts new file mode 100644 index 000000000..847fb64f1 --- /dev/null +++ b/source/packages/@aws-accelerator/tester/test/external-pipeline-account-tester-stack.test.ts @@ -0,0 +1,331 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { CONFIG_FILE_CONTENT_TYPE, CONFIG_FILE_NAME, TesterStack } from '../lib/tester-stack'; +import * as yaml from 'js-yaml'; +import * as fs from 'fs'; +import * as path from 'path'; +import { pascalCase } from 'pascal-case'; + +const testNamePrefix = 'Construct(TesterStack): '; + +/** + * External pipeline account TesterStack + */ +const app = new cdk.App({ + context: { + account: '333333333333', + region: 'us-east-1', + 'management-cross-account-role': 'AWSControlTowerExecution', + 'config-dir': path.join(__dirname, 'configs'), + qualifier: 'aws-accelerator', + 'management-account-id': '111111111111', + 'management-account-role-name': 'AcceleratorAccountAccessRole', + }, +}); + +const account = app.node.tryGetContext('account'); +const region = app.node.tryGetContext('region'); +const qualifier = app.node.tryGetContext('qualifier') ?? 'aws-accelerator'; +const managementCrossAccountRoleName = app.node.tryGetContext('management-cross-account-role-name'); +const configDirPath = app.node.tryGetContext('config-dir'); + +const configFilePath = path.join(configDirPath, CONFIG_FILE_NAME); +const configFileContent = yaml.load(fs.readFileSync(configFilePath, 'utf8')) as CONFIG_FILE_CONTENT_TYPE; + +const qualifierInPascalCase = pascalCase(qualifier) + .split('_') + .join('-') + .replace(/AwsAccelerator/gi, 'AWSAccelerator'); + +const stack = new TesterStack(app, `${qualifierInPascalCase}-TesterStack-${account}-${region}`, { + synthesizer: new cdk.DefaultStackSynthesizer({ + generateBootstrapVersionRule: false, + }), + managementCrossAccountRoleName: managementCrossAccountRoleName, + configFileContent: configFileContent, + qualifier: qualifier, + managementAccountId: app.node.tryGetContext('management-account-id'), + managementAccountRoleName: app.node.tryGetContext('management-account-role-name'), +}); + +/** + * ExternalPipelineAccount-TesterStack construct test + */ +describe('ExternalPipelineAccount-TesterStack', () => { + /** + * Number of ConfigRule resource test + */ + test(`${testNamePrefix} ConfigRule resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Config::ConfigRule', 1); + }); + + /** + * Number of Lambda function resource test + */ + test(`${testNamePrefix} Lambda function resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 1); + }); + + /** + * Number of Lambda permission resource test + */ + test(`${testNamePrefix} Lambda permission resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::Lambda::Permission', 1); + }); + + /** + * Number of Lambda IAM role resource test + */ + test(`${testNamePrefix} Lambda IAM role resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 1); + }); + + /** + * Number of Lambda IAM policy resource test + */ + test(`${testNamePrefix} Lambda IAM policy resource count test`, () => { + cdk.assertions.Template.fromStack(stack).resourceCountIs('AWS::IAM::Policy', 1); + }); + + /** + * ConfigRule awsacceleratorvalidatemaintransitgatewayCustomRule resource configuration test + */ + test(`${testNamePrefix} ConfigRule awsacceleratorvalidatemaintransitgatewayCustomRule resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + awsacceleratorvalidatemaintransitgatewayCustomRuleB70A49C4: { + Type: 'AWS::Config::ConfigRule', + DependsOn: [ + 'awsacceleratorvalidatemaintransitgatewayFunctionPermissionD97964A9', + 'awsacceleratorvalidatemaintransitgatewayFunction5DC44F8F', + 'awsacceleratorvalidatemaintransitgatewayFunctionServiceRoleDefaultPolicy21AEB3E1', + 'awsacceleratorvalidatemaintransitgatewayFunctionServiceRoleB1766D38', + ], + Properties: { + ConfigRuleName: 'aws-accelerator-validate-main-transit-gateway', + Description: 'Validate Main Transit Gateway', + InputParameters: { + awsConfigRegion: { + Ref: 'AWS::Region', + }, + managementAccount: { + id: '111111111111', + partition: { + Ref: 'AWS::Partition', + }, + roleName: 'AcceleratorAccountAccessRole', + }, + test: { + description: 'Validate Main Transit Gateway', + expect: 'PASS', + name: 'validate main transit gateway', + parameters: { + accountId: '333333333333', + amazonSideAsn: '65521', + autoAcceptSharingAttachments: 'enable', + defaultRouteTableAssociation: 'disable', + defaultRouteTablePropagation: 'disable', + dnsSupport: 'enable', + name: 'Main', + region: 'us-east-1', + routeTableNames: ['core', 'segregated', 'shared', 'standalone'], + shareTargetAccountIds: ['111111111111', '222222222222'], + vpnEcmpSupport: 'enable', + }, + suite: 'network', + testTarget: 'validateTransitGateway', + }, + }, + MaximumExecutionFrequency: 'Six_Hours', + Source: { + Owner: 'CUSTOM_LAMBDA', + SourceDetails: [ + { + EventSource: 'aws.config', + MaximumExecutionFrequency: 'Six_Hours', + MessageType: 'ScheduledNotification', + }, + ], + SourceIdentifier: { + 'Fn::GetAtt': ['awsacceleratorvalidatemaintransitgatewayFunction5DC44F8F', 'Arn'], + }, + }, + }, + }, + }, + }); + }); + + /** + * Lambda function awsacceleratorvalidatemaintransitgatewayFunction resource configuration test + */ + test(`${testNamePrefix} Lambda function awsacceleratorvalidatemaintransitgatewayFunction resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + awsacceleratorvalidatemaintransitgatewayFunction5DC44F8F: { + Type: 'AWS::Lambda::Function', + DependsOn: [ + 'awsacceleratorvalidatemaintransitgatewayFunctionServiceRoleDefaultPolicy21AEB3E1', + 'awsacceleratorvalidatemaintransitgatewayFunctionServiceRoleB1766D38', + ], + Properties: { + Code: { + S3Bucket: { + 'Fn::Sub': 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', + }, + }, + Description: 'AWS Config custom rule function used for test case "validate main transit gateway"', + Handler: 'index.handler', + Role: { + 'Fn::GetAtt': ['awsacceleratorvalidatemaintransitgatewayFunctionServiceRoleB1766D38', 'Arn'], + }, + Runtime: 'nodejs14.x', + }, + }, + }, + }); + }); + + /** + * Lambda permission awsacceleratorvalidatemaintransitgatewayFunctionPermission resource configuration test + */ + test(`${testNamePrefix} Lambda permission awsacceleratorvalidatemaintransitgatewayFunctionPermission resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + awsacceleratorvalidatemaintransitgatewayFunctionPermissionD97964A9: { + Type: 'AWS::Lambda::Permission', + Properties: { + Action: 'lambda:InvokeFunction', + FunctionName: { + 'Fn::GetAtt': ['awsacceleratorvalidatemaintransitgatewayFunction5DC44F8F', 'Arn'], + }, + Principal: 'config.amazonaws.com', + SourceAccount: { + Ref: 'AWS::AccountId', + }, + }, + }, + }, + }); + }); + + /** + * IAM role awsacceleratorvalidatemaintransitgatewayFunctionServiceRole resource configuration test + */ + test(`${testNamePrefix} IAM role awsacceleratorvalidatemaintransitgatewayFunctionServiceRole resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + awsacceleratorvalidatemaintransitgatewayFunctionServiceRoleB1766D38: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + ], + ], + }, + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::aws:policy/ReadOnlyAccess', + ], + ], + }, + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::aws:policy/service-role/AWSConfigRulesExecutionRole', + ], + ], + }, + ], + }, + }, + }, + }); + }); + + /** + * IAM policy awsacceleratorvalidatemaintransitgatewayFunctionServiceRoleDefaultPolicy resource configuration test + */ + test(`${testNamePrefix} IAM policy awsacceleratorvalidatemaintransitgatewayFunctionServiceRoleDefaultPolicy resource configuration test`, () => { + cdk.assertions.Template.fromStack(stack).templateMatches({ + Resources: { + awsacceleratorvalidatemaintransitgatewayFunctionServiceRoleDefaultPolicy21AEB3E1: { + Type: 'AWS::IAM::Policy', + Properties: { + PolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::111111111111:role/AcceleratorAccountAccessRole', + ], + ], + }, + Sid: 'LambdaSTSActions', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'awsacceleratorvalidatemaintransitgatewayFunctionServiceRoleDefaultPolicy21AEB3E1', + Roles: [ + { + Ref: 'awsacceleratorvalidatemaintransitgatewayFunctionServiceRoleB1766D38', + }, + ], + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/tester/test/tester-stack.test.ts b/source/packages/@aws-accelerator/tester/test/tester-stack.test.ts new file mode 100644 index 000000000..280fca26d --- /dev/null +++ b/source/packages/@aws-accelerator/tester/test/tester-stack.test.ts @@ -0,0 +1,329 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from 'aws-cdk-lib'; +import { CONFIG_FILE_CONTENT_TYPE, CONFIG_FILE_NAME, TesterStack } from '../lib/tester-stack'; +import * as yaml from 'js-yaml'; +import * as fs from 'fs'; +import * as path from 'path'; +import { pascalCase } from 'pascal-case'; + +const testNamePrefix = 'Construct(TesterStack): '; + +/** + * External pipeline account TesterStack + */ +const app = new cdk.App({ + context: { + account: '333333333333', + region: 'us-east-1', + 'management-cross-account-role': 'AWSControlTowerExecution', + 'config-dir': path.join(__dirname, 'configs'), + }, +}); + +const account = app.node.tryGetContext('account'); +const region = app.node.tryGetContext('region'); +const qualifier = app.node.tryGetContext('qualifier') ?? 'aws-accelerator'; +const managementCrossAccountRoleName = app.node.tryGetContext('management-cross-account-role-name'); +const configDirPath = app.node.tryGetContext('config-dir'); + +const configFilePath = path.join(configDirPath, CONFIG_FILE_NAME); +const configFileContent = yaml.load(fs.readFileSync(configFilePath, 'utf8')) as CONFIG_FILE_CONTENT_TYPE; + +const qualifierInPascalCase = pascalCase(qualifier) + .split('_') + .join('-') + .replace(/AwsAccelerator/gi, 'AWSAccelerator'); + +const externalAccountTesterStack = new TesterStack(app, `${qualifierInPascalCase}-TesterStack-${account}-${region}`, { + synthesizer: new cdk.DefaultStackSynthesizer({ + generateBootstrapVersionRule: false, + }), + managementCrossAccountRoleName: managementCrossAccountRoleName, + configFileContent: configFileContent, + qualifier: qualifier, + managementAccountId: app.node.tryGetContext('management-account-id'), + managementAccountRoleName: app.node.tryGetContext('management-account-role-name'), +}); + +/** + * TesterStack construct test + */ +describe('TesterStack', () => { + /** + * Number of ConfigRule resource test + */ + test(`${testNamePrefix} ConfigRule resource count test`, () => { + cdk.assertions.Template.fromStack(externalAccountTesterStack).resourceCountIs('AWS::Config::ConfigRule', 1); + }); + + /** + * Number of Lambda function resource test + */ + test(`${testNamePrefix} Lambda function resource count test`, () => { + cdk.assertions.Template.fromStack(externalAccountTesterStack).resourceCountIs('AWS::Lambda::Function', 1); + }); + + /** + * Number of Lambda permission resource test + */ + test(`${testNamePrefix} Lambda permission resource count test`, () => { + cdk.assertions.Template.fromStack(externalAccountTesterStack).resourceCountIs('AWS::Lambda::Permission', 1); + }); + + /** + * Number of Lambda IAM role resource test + */ + test(`${testNamePrefix} Lambda IAM role resource count test`, () => { + cdk.assertions.Template.fromStack(externalAccountTesterStack).resourceCountIs('AWS::IAM::Role', 1); + }); + + /** + * Number of Lambda IAM policy resource test + */ + test(`${testNamePrefix} Lambda IAM policy resource count test`, () => { + cdk.assertions.Template.fromStack(externalAccountTesterStack).resourceCountIs('AWS::IAM::Policy', 1); + }); + + /** + * ConfigRule awsacceleratorvalidatemaintransitgatewayCustomRule resource configuration test + */ + test(`${testNamePrefix} ConfigRule awsacceleratorvalidatemaintransitgatewayCustomRule resource configuration test`, () => { + cdk.assertions.Template.fromStack(externalAccountTesterStack).templateMatches({ + Resources: { + awsacceleratorvalidatemaintransitgatewayCustomRuleB70A49C4: { + Type: 'AWS::Config::ConfigRule', + DependsOn: [ + 'awsacceleratorvalidatemaintransitgatewayFunctionPermissionD97964A9', + 'awsacceleratorvalidatemaintransitgatewayFunction5DC44F8F', + 'awsacceleratorvalidatemaintransitgatewayFunctionServiceRoleDefaultPolicy21AEB3E1', + 'awsacceleratorvalidatemaintransitgatewayFunctionServiceRoleB1766D38', + ], + Properties: { + ConfigRuleName: 'aws-accelerator-validate-main-transit-gateway', + Description: 'Validate Main Transit Gateway', + InputParameters: { + awsConfigRegion: { + Ref: 'AWS::Region', + }, + managementAccount: { + id: { + Ref: 'AWS::AccountId', + }, + partition: { + Ref: 'AWS::Partition', + }, + }, + test: { + description: 'Validate Main Transit Gateway', + expect: 'PASS', + name: 'validate main transit gateway', + parameters: { + accountId: '333333333333', + amazonSideAsn: '65521', + autoAcceptSharingAttachments: 'enable', + defaultRouteTableAssociation: 'disable', + defaultRouteTablePropagation: 'disable', + dnsSupport: 'enable', + name: 'Main', + region: 'us-east-1', + routeTableNames: ['core', 'segregated', 'shared', 'standalone'], + shareTargetAccountIds: ['111111111111', '222222222222'], + vpnEcmpSupport: 'enable', + }, + suite: 'network', + testTarget: 'validateTransitGateway', + }, + }, + MaximumExecutionFrequency: 'Six_Hours', + Source: { + Owner: 'CUSTOM_LAMBDA', + SourceDetails: [ + { + EventSource: 'aws.config', + MaximumExecutionFrequency: 'Six_Hours', + MessageType: 'ScheduledNotification', + }, + ], + SourceIdentifier: { + 'Fn::GetAtt': ['awsacceleratorvalidatemaintransitgatewayFunction5DC44F8F', 'Arn'], + }, + }, + }, + }, + }, + }); + }); + + /** + * Lambda function awsacceleratorvalidatemaintransitgatewayFunction resource configuration test + */ + test(`${testNamePrefix} Lambda function awsacceleratorvalidatemaintransitgatewayFunction resource configuration test`, () => { + cdk.assertions.Template.fromStack(externalAccountTesterStack).templateMatches({ + Resources: { + awsacceleratorvalidatemaintransitgatewayFunction5DC44F8F: { + Type: 'AWS::Lambda::Function', + DependsOn: [ + 'awsacceleratorvalidatemaintransitgatewayFunctionServiceRoleDefaultPolicy21AEB3E1', + 'awsacceleratorvalidatemaintransitgatewayFunctionServiceRoleB1766D38', + ], + Properties: { + Code: { + S3Bucket: { + 'Fn::Sub': 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', + }, + }, + Description: 'AWS Config custom rule function used for test case "validate main transit gateway"', + Handler: 'index.handler', + Role: { + 'Fn::GetAtt': ['awsacceleratorvalidatemaintransitgatewayFunctionServiceRoleB1766D38', 'Arn'], + }, + Runtime: 'nodejs14.x', + }, + }, + }, + }); + }); + + /** + * Lambda permission awsacceleratorvalidatemaintransitgatewayFunctionPermission resource configuration test + */ + test(`${testNamePrefix} Lambda permission awsacceleratorvalidatemaintransitgatewayFunctionPermission resource configuration test`, () => { + cdk.assertions.Template.fromStack(externalAccountTesterStack).templateMatches({ + Resources: { + awsacceleratorvalidatemaintransitgatewayFunctionPermissionD97964A9: { + Type: 'AWS::Lambda::Permission', + Properties: { + Action: 'lambda:InvokeFunction', + FunctionName: { + 'Fn::GetAtt': ['awsacceleratorvalidatemaintransitgatewayFunction5DC44F8F', 'Arn'], + }, + Principal: 'config.amazonaws.com', + SourceAccount: { + Ref: 'AWS::AccountId', + }, + }, + }, + }, + }); + }); + + /** + * IAM role awsacceleratorvalidatemaintransitgatewayFunctionServiceRole resource configuration test + */ + test(`${testNamePrefix} IAM role awsacceleratorvalidatemaintransitgatewayFunctionServiceRole resource configuration test`, () => { + cdk.assertions.Template.fromStack(externalAccountTesterStack).templateMatches({ + Resources: { + awsacceleratorvalidatemaintransitgatewayFunctionServiceRoleB1766D38: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + ], + ], + }, + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::aws:policy/ReadOnlyAccess', + ], + ], + }, + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::aws:policy/service-role/AWSConfigRulesExecutionRole', + ], + ], + }, + ], + }, + }, + }, + }); + }); + + /** + * IAM policy awsacceleratorvalidatemaintransitgatewayFunctionServiceRoleDefaultPolicy resource configuration test + */ + test(`${testNamePrefix} IAM policy awsacceleratorvalidatemaintransitgatewayFunctionServiceRoleDefaultPolicy resource configuration test`, () => { + cdk.assertions.Template.fromStack(externalAccountTesterStack).templateMatches({ + Resources: { + awsacceleratorvalidatemaintransitgatewayFunctionServiceRoleDefaultPolicy21AEB3E1: { + Type: 'AWS::IAM::Policy', + Properties: { + PolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::*:role/undefined', + ], + ], + }, + Sid: 'LambdaSTSActions', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'awsacceleratorvalidatemaintransitgatewayFunctionServiceRoleDefaultPolicy21AEB3E1', + Roles: [ + { + Ref: 'awsacceleratorvalidatemaintransitgatewayFunctionServiceRoleB1766D38', + }, + ], + }, + }, + }, + }); + }); +}); diff --git a/source/packages/@aws-accelerator/tester/tsconfig.json b/source/packages/@aws-accelerator/tester/tsconfig.json new file mode 100644 index 000000000..530442618 --- /dev/null +++ b/source/packages/@aws-accelerator/tester/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + }, + "include": ["lib/*.ts", + "bin/**/*","index.ts" + ], + "exclude": ["cdk.out/**/*", "test/**/*"] +} diff --git a/source/packages/@aws-accelerator/tools/index.ts b/source/packages/@aws-accelerator/tools/index.ts new file mode 100644 index 000000000..5df743535 --- /dev/null +++ b/source/packages/@aws-accelerator/tools/index.ts @@ -0,0 +1,15 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +export * from './lib/classes/accelerator-tool'; +export * from './uninstaller'; diff --git a/source/packages/@aws-accelerator/tools/lib/classes/accelerator-tool.ts b/source/packages/@aws-accelerator/tools/lib/classes/accelerator-tool.ts new file mode 100644 index 000000000..2fd07faf8 --- /dev/null +++ b/source/packages/@aws-accelerator/tools/lib/classes/accelerator-tool.ts @@ -0,0 +1,1557 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +import { AcceleratorStackNames } from '../../../accelerator/lib/accelerator'; +import { Logger } from '../../../accelerator/lib/logger'; +// import { AcceleratorStackNames, Logger } from '@aws-accelerator/accelerator'; +import { GlobalConfig } from '@aws-accelerator/config'; +import { throttlingBackOff } from '@aws-accelerator/utils'; + +import { + CloudFormationClient, + DeleteStackCommand, + DeleteStackCommandOutput, + DescribeStacksCommand, + ListStackResourcesCommand, + Stack, + UpdateTerminationProtectionCommand, + waitUntilStackDeleteComplete, + waitUntilStackUpdateComplete, +} from '@aws-sdk/client-cloudformation'; +import { CloudWatchLogsClient, DeleteLogGroupCommand, DescribeLogGroupsCommand } from '@aws-sdk/client-cloudwatch-logs'; +import { + BatchDeleteBuildsCommand, + BatchGetProjectsCommand, + CodeBuildClient, + ListBuildsForProjectCommand, +} from '@aws-sdk/client-codebuild'; +import { CodeCommitClient, DeleteRepositoryCommand, GetFileCommand } from '@aws-sdk/client-codecommit'; +import { CodePipelineClient, GetPipelineCommand } from '@aws-sdk/client-codepipeline'; +import { + DescribeKeyCommand, + DisableKeyCommand, + KeyState, + KMSClient, + ScheduleKeyDeletionCommand, +} from '@aws-sdk/client-kms'; +import { ListAccountsCommand, OrganizationsClient } from '@aws-sdk/client-organizations'; +import { + DeleteBucketCommand, + DeleteObjectsCommand, + ListBucketsCommand, + ListObjectVersionsCommand, + S3Client, +} from '@aws-sdk/client-s3'; +import { AssumeRoleCommand, Credentials, GetCallerIdentityCommand, STSClient } from '@aws-sdk/client-sts'; +import { WaiterResult } from '@aws-sdk/util-waiter'; +import { BackupClient, DeleteBackupVaultCommand } from '@aws-sdk/client-backup'; + +/** + * Type for pipeline stage action information with order and action name + */ +export type stageActionType = { order: number; name: string }; + +/** + * Pipeline Stack Type + */ +export type pipelineStackType = { + stageOrder: number; + order: number; + stackName: string; + accountId: string; +}; + +/** + * Pipeline Management Account Type, with account ID, role name to assume and sts credential + */ +export type ManagementAccountType = + | { + accountId: string; + assumeRoleName: string | undefined; + credentials: Credentials | undefined; + } + | undefined; + +type stackPersistentObjectListType = { + stackName: string; + resourceType: 'S3' | 'CWLogs' | 'KMS'; + resourceClient: KMSClient | CloudWatchLogsClient | S3Client | BackupClient; + resourcePhysicalId: string; +}; + +/** + * Accelerator AcceleratorToolProps + */ +export interface AcceleratorToolProps { + readonly installerStackName: string; + readonly partition: string; + readonly keepBootstraps: boolean; + readonly deleteData: boolean; + readonly deleteConfigRepo: boolean; + readonly deletePipelines: boolean; + readonly ignoreTerminationProtection: boolean; +} + +/** + * AcceleratorTool Class + */ +export class AcceleratorTool { + /** + * Executing Account ID + * @private + */ + private executingAccountId: string | undefined; + /** + * Pipeline Global Config + * @private + */ + private globalConfig: GlobalConfig | undefined; + + /** + * acceleratorToolProps + * @private + */ + private readonly acceleratorToolProps: AcceleratorToolProps; + + /** + * Pipeline Source Config repository details + * @private + */ + private pipelineConfigSourceRepo: { repositoryName: string; branch: string; provider: string } | undefined; + + /** + * bootstrapBuildEnvironmentVariables + * @private + */ + private bootstrapBuildEnvironmentVariables: { name: string; value: string }[] | undefined; + + /** + * organizationAccounts - for list of accounts in organization + * @private + */ + private organizationAccounts: { + accountName: string; + accountId: string; + }[] = []; + + /** + * multiActionStageActions + * @private + */ + private multiActionStageActions: stageActionType[] = []; + + /** + * pipelineStageActions + * @private + */ + private pipelineStageActions: { + stage: string; + order: number; + actions: stageActionType[]; + }[] = []; + + /** + * pipelineManagementAccount + * @private + */ + private pipelineManagementAccount: ManagementAccountType = undefined; + + /** + * externalPipelineAccount object + * @private + */ + private externalPipelineAccount: { isUsed: boolean; accountId: string | undefined } = { + isUsed: false, + accountId: undefined, + }; + + /** + * acceleratorCloudFormationStacks + * @private + */ + private acceleratorCloudFormationStacks: { + stageOrder: number; + order: number; + stackName: string; + accountId: string; + }[] = []; + + private acceleratorCodeBuildProjects: string[] = []; + + private prepareStackPersistentObjectList: stackPersistentObjectListType[] = []; + + constructor(props: AcceleratorToolProps) { + this.acceleratorToolProps = props; + } + + /** + * Function to uninstall accelerator. It is expected to completely rollback installer. + * Uninstaller can rollback following resources created by accelerator. + *
    + *
  • CloudFormation Stacks + *
  • S3 Buckets + *
  • Cloudwatch Log Groups + *
  • Codebuild Projects + *
  • CodeCommit Repository + *
  • CodePipeline + *
+ * @param installerStackName + * The name of the installer cloudformation stack + */ + public async uninstallAccelerator(installerStackName: string): Promise { + // Get executing account ID + const response = await throttlingBackOff(() => new STSClient({}).send(new GetCallerIdentityCommand({}))); + this.executingAccountId = response.Account; + + // Get installer pipeline + const installerPipeline = await AcceleratorTool.getPipelineNameFromCloudFormationStack(installerStackName); + if (!installerPipeline.status) { + Logger.debug(`[accelerator-tool] ${installerPipeline.pipelineName}`); + return false; + } + + const getPipelineNameResponse = await throttlingBackOff(() => + new CodePipelineClient({}).send(new GetPipelineCommand({ name: installerPipeline!.pipelineName })), + ); + + const installerCodeBuildProjectName = + getPipelineNameResponse.pipeline!.stages![1].actions![0].configuration!['ProjectName']; + + const batchGetProjectsCommandResponse = await throttlingBackOff(() => + new CodeBuildClient({}).send(new BatchGetProjectsCommand({ names: [installerCodeBuildProjectName] })), + ); + + // Default assignments when no qualifier present + let acceleratorQualifier = 'AWSAccelerator'; + let acceleratorPipelineStackNamePrefix = 'AWSAccelerator-PipelineStack'; + let acceleratorPipelineName = 'AWSAccelerator-Pipeline'; + + // Accelerator tester configuration + let testerPipelineStackNamePrefix = 'AWSAccelerator-TesterPipelineStack'; + let testerPipelineConfigRepositoryName = 'aws-accelerator-test-config'; + let testerStackNamePrefix = 'AWSAccelerator-TesterStack'; + // End of default assignments + + for (const envVariable of batchGetProjectsCommandResponse.projects![0].environment!.environmentVariables!) { + if (envVariable.name === 'ACCELERATOR_QUALIFIER') { + acceleratorQualifier = envVariable.value!; + acceleratorPipelineStackNamePrefix = `${envVariable.value!}-pipeline-stack`; + acceleratorPipelineName = `${envVariable.value!}-pipeline`; + testerStackNamePrefix = `${envVariable.value!}-tester-stack`; + testerPipelineStackNamePrefix = `${envVariable.value!}-tester-pipeline-stack`; + testerPipelineConfigRepositoryName = `${envVariable.value!}-test-config`; + break; + } + } + + //Delete accelerator target cloudformation stacks + await this.deleteAcceleratorTargetCloudFormationStacks(acceleratorPipelineName); + + await this.deletePrepareStackResources(); + + // return false; + + // Installer and Tester stack resource cleanup takes place in pipeline or management account, so reset the credential settings + AcceleratorTool.resetCredentialEnvironment(); + + // Delete tester stack + await this.deleteTesterStack(testerStackNamePrefix); + + // Delete tester pipeline stack + await this.deleteTesterPipelineStack(testerPipelineStackNamePrefix, testerPipelineConfigRepositoryName); + + // Delete Accelerator Pipeline stack + await this.deleteAcceleratorPipelineStack(acceleratorPipelineStackNamePrefix); + + //Delete Installer Stack + await this.deleteAcceleratorInstallerStack(installerStackName); + + // Cleanup any remaining resources in all accounts, this is required because, + // during deletion of custom resources creates cloudwatch logs which is required to clean + // Only when full cleanup was intended this this cleanup should take place + if (this.acceleratorToolProps.ignoreTerminationProtection) { + await this.deleteAcceleratorRemainingResourcesInAllAccounts(acceleratorQualifier); + } + + //TODO Delete bootstrap stack for management account home region after pipeline deleted only when pipeline management account is not used + // Delete bootstrap stack from home region for management or pipeline account as last step of cleanups + if (!this.acceleratorToolProps.keepBootstraps && !this.externalPipelineAccount.isUsed) { + AcceleratorTool.resetCredentialEnvironment(); + await this.deleteStack(new CloudFormationClient({}), 'AWSAccelerator-CDKToolkit'); + } + return true; + } + + /** + * Clears credential environment variables + * @private + */ + private static resetCredentialEnvironment() { + //reset credential variables + delete process.env['AWS_ACCESS_KEY_ID']; + delete process.env['AWS_ACCESS_KEY']; + delete process.env['AWS_SECRET_KEY']; + delete process.env['AWS_SECRET_ACCESS_KEY']; + delete process.env['AWS_SESSION_TOKEN']; + } + + /** + * Delete installer stack and resources like installer stack, installer pipeline code build projects etc. + * @param installerStackName + * @private + */ + private async deleteAcceleratorInstallerStack(installerStackName: string): Promise { + if (!this.acceleratorToolProps.deletePipelines) { + return true; + } + + const installerPipeline = await AcceleratorTool.getPipelineNameFromCloudFormationStack(installerStackName); + + if (installerPipeline.status) { + const codeBuildClient = new CodeBuildClient({}); + const response = await throttlingBackOff(() => + codeBuildClient.send(new BatchGetProjectsCommand({ names: [installerPipeline.pipelineName] })), + ); + for (const project of response.projects ?? []) { + await this.deleteCodeBuilds(codeBuildClient, project.name!); + } + } + await this.deleteStack(new CloudFormationClient({}), installerStackName); + + return true; + } + + /** + * Function to delete cloudformation stacks created by accelerator pipeline + * @param pipelineName + * @private + */ + private async deleteAcceleratorTargetCloudFormationStacks(pipelineName: string): Promise { + if (await this.initPipeline(pipelineName)) { + const groupBy = (array: pipelineStackType[], key1: string, key2: string) => { + return array.reduce((accumulator, currentValue) => { + // @ts-ignore + if (!accumulator[currentValue[key1] + '' + currentValue[key2]]) { + // @ts-ignore + accumulator[currentValue[key1] + '' + currentValue[key2]] = []; + } + // @ts-ignore + accumulator[currentValue[key1] + '' + currentValue[key2]].push({ + accountId: currentValue.accountId, + stackName: currentValue.stackName, + }); + return accumulator; + }, {}); + }; + const stacksGroupByAccounts = groupBy(this.acceleratorCloudFormationStacks, 'stageOrder', 'order'); + + let pipelineStacksInDeleteOrder: { + deleteOrder: number; + displayOrderString: string; + resourceDetails: [{ accountId: string; stackName: string }]; + }[] = []; + + for (const [key, value] of Object.entries(stacksGroupByAccounts)) { + pipelineStacksInDeleteOrder.push({ + deleteOrder: parseInt(key), + displayOrderString: [...key].map(Number).join('.'), + resourceDetails: value as [{ accountId: string; stackName: string }], + }); + } + + // Sort it descending to create delete stack commands + pipelineStacksInDeleteOrder = pipelineStacksInDeleteOrder.sort((first, second) => + 0 - first.deleteOrder > second.deleteOrder ? 1 : -1, + ); + + for (const accountStack of pipelineStacksInDeleteOrder) { + Logger.info(`[accelerator-tool] >>>>> Step - ${accountStack.displayOrderString} deletion started`); + if (!(await this.deleteStacks(accountStack))) { + return false; + } + Logger.info(`[accelerator-tool] >>>>> Step - ${accountStack.displayOrderString} deletion completed`); + Logger.info(''); + } + return true; + } else { + return false; + } + } + + /** + * Private async function to initialize required properties to perform accelerator cleanup + * @private + */ + private async initPipeline(pipelineName: string): Promise { + try { + const response = await throttlingBackOff(() => + new CodePipelineClient({}).send(new GetPipelineCommand({ name: pipelineName })), + ); + + let orderCounter = 0; + for (const stage of response.pipeline!.stages!) { + // maintain list of code build project for the accelerator pipeline + for (const action of stage.actions!) { + if ( + action.configuration!['ProjectName'] && + !this.acceleratorCodeBuildProjects.find(item => item === action.configuration!['ProjectName']) + ) { + this.acceleratorCodeBuildProjects.push(action.configuration!['ProjectName']); + } + } + + // Get the source repo names + if (stage.name === 'Source') { + for (const action of stage.actions!) { + switch (action.name!) { + case 'Configuration': + this.pipelineConfigSourceRepo = { + repositoryName: action.configuration!['RepositoryName'], + branch: action.configuration!['BranchName'], + provider: action.actionTypeId!.provider!, + }; + break; + } + } + } + // Get bootstrap environment variables + if (stage.name === 'Bootstrap') { + this.bootstrapBuildEnvironmentVariables = await this.getCodeBuildEnvironmentVariables( + stage.actions![0].configuration!['ProjectName'], + ); + } + + // Get Accelerator stage actions + if (stage.name !== 'Source' && stage.name !== 'Build' && stage.name !== 'Review') { + if (stage.actions!.length > 1) { + for (const action of stage.actions!) { + const environmentVariableJson = Object.values( + JSON.parse(action.configuration!['EnvironmentVariables']), + )[0]; + + const environmentDeployCommand = environmentVariableJson as { + name: string; + type: string; + value: string; + }; + + const stackName = AcceleratorStackNames[environmentDeployCommand.value.split(' ')[2]]; + this.multiActionStageActions.push({ + order: action.runOrder!, + name: stackName, + }); + } + + // Sort it descending to create delete stack commands + this.multiActionStageActions = this.multiActionStageActions.sort((first, second) => + 0 - first.order > second.order ? 1 : -1, + ); + + this.pipelineStageActions.push({ + stage: stage.name!, + order: (orderCounter += 1), + actions: this.multiActionStageActions, + }); + } else { + for (const action of stage.actions!) { + const environmentVariableJson = Object.values( + JSON.parse(action.configuration!['EnvironmentVariables']), + )[0]; + const environmentDeployCommand = environmentVariableJson as { + name: string; + type: string; + value: string; + }; + + let stackName: string | undefined; + if (environmentDeployCommand.value.split(' ')[0] === 'bootstrap') { + if (this.acceleratorToolProps.keepBootstraps) { + stackName = undefined; + } else { + stackName = 'AWSAccelerator-CDKToolkit'; + } + } else { + stackName = AcceleratorStackNames[environmentDeployCommand.value.split(' ')[2]]; + } + + if (stackName) { + this.pipelineStageActions.push({ + stage: stage.name!, + order: (orderCounter += 1), + actions: [ + { + order: 1, + name: stackName, + }, + ], + }); + } + } + } + } + } + + // Sort it descending to create delete stack commands + this.pipelineStageActions = this.pipelineStageActions.sort((first, second) => + 0 - first.order > second.order ? 1 : -1, + ); + + // Get Pipeline Global config + this.globalConfig = await this.getGlobalConfig(); + + // Set pipeline management account details + this.pipelineManagementAccount = await this.getPipelineManagementAccount(); + + // Get List of Accounts within organization + this.organizationAccounts = await this.getOrganizationAccountList(); + + // Order the stacks in delete order + this.acceleratorCloudFormationStacks = this.getPipelineCloudFormationStacks(); + + // console.log(this.acceleratorCloudFormationStacks); + // process.exit(1); + + return true; + } catch (e) { + console.log(e); + return false; + } + } + + /** + * Function to delete accelerator pipeline stack and it's resources + * @param acceleratorPipelineStackNamePrefix + * @private + */ + private async deleteAcceleratorPipelineStack(acceleratorPipelineStackNamePrefix: string): Promise { + if (!this.acceleratorToolProps.deletePipelines) { + return true; + } + const acceleratorPipelineStackName = `${acceleratorPipelineStackNamePrefix}-${ + this.externalPipelineAccount.isUsed + ? this.externalPipelineAccount.accountId! + : this.pipelineManagementAccount!.accountId + }-${this.globalConfig?.homeRegion}`; + + const acceleratorPipeline = await AcceleratorTool.getPipelineNameFromCloudFormationStack( + acceleratorPipelineStackName, + ); + + if (acceleratorPipeline.status) { + if (this.acceleratorToolProps.deleteConfigRepo) { + await this.deleteCodecommitRepository( + new CodeCommitClient({}), + `${this.pipelineConfigSourceRepo?.repositoryName}`, + ); + } + + const codeBuildClient = new CodeBuildClient({}); + const response = await throttlingBackOff(() => + codeBuildClient.send(new BatchGetProjectsCommand({ names: [acceleratorPipeline.pipelineName] })), + ); + for (const project of response.projects ?? []) { + await this.deleteCodeBuilds(codeBuildClient, project.name!); + } + } + + await this.deleteStack(new CloudFormationClient({}), acceleratorPipelineStackName); + + return true; + } + + /** + * Function to get code build environment variables + * @param buildProjectName + * @private + */ + private async getCodeBuildEnvironmentVariables( + buildProjectName: string, + ): Promise<{ name: string; value: string }[] | undefined> { + const buildEnvironmentVariables: { name: string; value: string }[] = []; + const codeBuildClient = new CodeBuildClient({}); + const response = await throttlingBackOff(() => + codeBuildClient.send(new BatchGetProjectsCommand({ names: [buildProjectName] })), + ); + + for (const envVariable of response.projects![0].environment!.environmentVariables!) { + buildEnvironmentVariables.push({ name: envVariable.name!, value: envVariable.value! }); + } + + return buildEnvironmentVariables; + } + + /** + * Get pipeline management account details + * @private + */ + private async getPipelineManagementAccount(): Promise { + let managementAccountId: string | undefined; + let managementAccountRoleName: string | undefined; + let managementAccountCredentials: Credentials | undefined; + + for (const envVariable of this.bootstrapBuildEnvironmentVariables!) { + if (envVariable.name === 'MANAGEMENT_ACCOUNT_ID') { + managementAccountId = envVariable.value; + } + if (envVariable.name === 'MANAGEMENT_ACCOUNT_ROLE_NAME') { + managementAccountRoleName = envVariable.value; + } + } + + if (this.executingAccountId !== managementAccountId && managementAccountId && managementAccountRoleName) { + managementAccountCredentials = await this.getManagementAccountCredentials( + managementAccountId!, + managementAccountRoleName, + ); + this.externalPipelineAccount = { isUsed: true, accountId: this.executingAccountId! }; + return { + accountId: managementAccountId, + assumeRoleName: managementAccountRoleName, + credentials: managementAccountCredentials, + }; + } else { + this.externalPipelineAccount = { isUsed: false, accountId: managementAccountId }; + return { + accountId: this.executingAccountId!, + assumeRoleName: undefined, + credentials: undefined, + }; + } + } + + /** + * Function to get account list from organization + * @private + */ + private async getOrganizationAccountList(): Promise<{ accountName: string; accountId: string }[]> { + let organizationsClient: OrganizationsClient; + if (this.pipelineManagementAccount!.credentials) { + organizationsClient = new OrganizationsClient({ + credentials: { + secretAccessKey: this.pipelineManagementAccount!.credentials.SecretAccessKey!, + accessKeyId: this.pipelineManagementAccount!.credentials.AccessKeyId!, + sessionToken: this.pipelineManagementAccount!.credentials.SessionToken!, + expiration: this.pipelineManagementAccount!.credentials.Expiration!, + }, + }); + } else { + organizationsClient = new OrganizationsClient({}); + } + + const accountIds: { accountName: string; accountId: string }[] = []; + let nextToken: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => + organizationsClient.send(new ListAccountsCommand({ NextToken: nextToken })), + ); + for (const account of page.Accounts ?? []) { + if (account.Id && account.Name) { + accountIds.push({ accountName: account.Name, accountId: account.Id }); + } + } + nextToken = page.NextToken; + } while (nextToken); + return accountIds; + } + + /** + * Function to get cloudformation stacks created by pipeline stage actions + * @private + */ + private getPipelineCloudFormationStacks(): pipelineStackType[] { + const pipelineCloudFormationStacks: pipelineStackType[] = []; + + for (const stage of this.pipelineStageActions) { + for (const action of stage.actions) { + for (const account of this.organizationAccounts) { + pipelineCloudFormationStacks.push({ + stageOrder: stage.order, + order: action.order, + accountId: account.accountId, + stackName: action.name, + }); + } + } + } + return pipelineCloudFormationStacks; + } + + /** + * Function to get management account credentials + * @param managementAccountId + * @param managementAccountRoleName + * @private + */ + private async getManagementAccountCredentials( + managementAccountId: string, + managementAccountRoleName: string | undefined, + ): Promise { + if (!managementAccountId && !managementAccountRoleName) { + return undefined; + } + + const roleArn = `arn:${this.acceleratorToolProps.partition}:iam::${managementAccountId}:role/${managementAccountRoleName}`; + + const stsClient = new STSClient({}); + + // Logger.info(`[accelerator-tool] management account roleArn => ${roleArn}`); + + const assumeRoleCredential = await throttlingBackOff(() => + stsClient.send( + new AssumeRoleCommand({ + RoleArn: roleArn, + RoleSessionName: 'acceleratorAssumeRoleSession', + DurationSeconds: 3600, + }), + ), + ); + + process.env['AWS_ACCESS_KEY_ID'] = assumeRoleCredential.Credentials!.AccessKeyId!; + process.env['AWS_ACCESS_KEY'] = assumeRoleCredential.Credentials!.AccessKeyId!; + + process.env['AWS_SECRET_KEY'] = assumeRoleCredential.Credentials!.SecretAccessKey!; + process.env['AWS_SECRET_ACCESS_KEY'] = assumeRoleCredential.Credentials!.SecretAccessKey!; + + process.env['AWS_SESSION_TOKEN'] = assumeRoleCredential.Credentials!.SessionToken; + + return assumeRoleCredential.Credentials; + } + + /** + * Function to clean remaining resources from all accounts, this is because custom resource creates cloudwatch log group during delete events. + * Which is not deleted by the cloudformation stack + * @param installerQualifier + * @private + */ + private async deleteAcceleratorRemainingResourcesInAllAccounts(installerQualifier: string): Promise { + if (this.pipelineManagementAccount!.credentials) { + process.env['AWS_ACCESS_KEY_ID'] = this.pipelineManagementAccount!.credentials.AccessKeyId!; + process.env['AWS_ACCESS_KEY'] = this.pipelineManagementAccount!.credentials.AccessKeyId!; + process.env['AWS_SECRET_KEY'] = this.pipelineManagementAccount!.credentials.SecretAccessKey!; + process.env['AWS_SECRET_ACCESS_KEY'] = this.pipelineManagementAccount!.credentials.SecretAccessKey!; + process.env['AWS_SESSION_TOKEN'] = this.pipelineManagementAccount!.credentials.SessionToken; + } + + const assumeRoleName = 'AWSControlTowerExecution'; + let cloudWatchLogsClient: CloudWatchLogsClient; + for (const region of this.globalConfig!.enabledRegions) { + for (const account of this.organizationAccounts) { + if (account.accountId !== this.pipelineManagementAccount!.accountId) { + const roleArn = `arn:${this.acceleratorToolProps.partition}:iam::${account.accountId}:role/${assumeRoleName}`; + const stsClient = new STSClient({ region: region }); + const assumeRoleCredential = await throttlingBackOff(() => + stsClient.send( + new AssumeRoleCommand({ + RoleArn: roleArn, + RoleSessionName: 'acceleratorAssumeRoleSession', + }), + ), + ); + cloudWatchLogsClient = new CloudWatchLogsClient({ + region: region, + credentials: { + accessKeyId: assumeRoleCredential.Credentials!.AccessKeyId!, + secretAccessKey: assumeRoleCredential.Credentials!.SecretAccessKey!, + sessionToken: assumeRoleCredential.Credentials!.SessionToken, + expiration: assumeRoleCredential.Credentials!.Expiration, + }, + }); + } else { + cloudWatchLogsClient = new CloudWatchLogsClient({ region: region }); + } + + // Clean all accelerator related cloud watch log groups + let nextToken: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => + cloudWatchLogsClient.send(new DescribeLogGroupsCommand({ nextToken })), + ); + for (const logGroup of page.logGroups!) { + if ( + logGroup.logGroupName?.includes('/aws/lambda/AWSAccelerator') || + // logGroup.logGroupName?.includes('/aws/lambda/Accelerator') || + logGroup.logGroupName?.includes(`/aws/codebuild/${installerQualifier}`) + ) { + Logger.info(`[accelerator-tool] Deleting log group ${logGroup.logGroupName}`); + await throttlingBackOff(() => + cloudWatchLogsClient.send(new DeleteLogGroupCommand({ logGroupName: logGroup.logGroupName })), + ); + } + } + nextToken = page.nextToken; + } while (nextToken); + } + } + return true; + } + + /** + * Function to get GlobalConfig object from the repo content + * @private + */ + private async getGlobalConfig(): Promise { + const codeCommitClient = new CodeCommitClient({}); + const response = await throttlingBackOff(() => + codeCommitClient.send( + new GetFileCommand({ + repositoryName: this.pipelineConfigSourceRepo!.repositoryName!, + filePath: 'global-config.yaml', + }), + ), + ); + const globalConfig = GlobalConfig.loadFromString(new TextDecoder('utf-8').decode(response.fileContent!)); + if (!globalConfig) { + Logger.warn('[accelerator-tool] Error parsing global-config file, object undefined'); + } + return globalConfig!; + } + + /** + * Function to delete bucket once all objects are deleted + * @param s3Client + * @param stackName + * @param bucketName + * @private + */ + private async deleteBucket(s3Client: S3Client, stackName: string, bucketName: string): Promise { + // List object and Delete objects + let listObjectVersionsResponse = await throttlingBackOff(() => + s3Client.send( + new ListObjectVersionsCommand({ + Bucket: bucketName, + }), + ), + ); + let contents = [ + ...(listObjectVersionsResponse.Versions ?? []), + ...(listObjectVersionsResponse.DeleteMarkers ?? []), + ]; + while (contents.length > 0) { + Logger.info(`[accelerator-tool] Number of objects in ${bucketName} is ${contents.length} will be deleted`); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const records = contents.map((record: any) => ({ + Key: record.Key, + VersionId: record.VersionId, + })); + Logger.info(`[accelerator-tool] Deleting objects from bucket ${bucketName} from ${stackName} stack`); + await throttlingBackOff(() => + s3Client.send( + new DeleteObjectsCommand({ + Bucket: bucketName, + Delete: { Objects: records }, + }), + ), + ); + + // Wait before checking again for data + await new Promise(func => setTimeout(func, 1000)); + + listObjectVersionsResponse = await throttlingBackOff(() => + s3Client.send( + new ListObjectVersionsCommand({ + Bucket: bucketName, + }), + ), + ); + + contents = [...(listObjectVersionsResponse.Versions ?? []), ...(listObjectVersionsResponse.DeleteMarkers ?? [])]; + + Logger.info(`[accelerator-tool] Number of objects in ${bucketName} is ${contents.length}, remaining to delete`); + } + + // Delete the empty Bucket + Logger.info(`[accelerator-tool] Deleting empty bucket ${bucketName} from ${stackName} stack`); + await throttlingBackOff(() => + s3Client.send( + new DeleteBucketCommand({ + Bucket: bucketName, + }), + ), + ); + + return true; + } + + /** + * Function to delete Cloudwatch log group + * @param cloudWatchLogsClient + * @param stackName + * @param logGroupName + * @private + */ + private async deleteCloudWatchLogs( + cloudWatchLogsClient: CloudWatchLogsClient, + stackName: string, + logGroupName: string, + ): Promise { + Logger.info(`[accelerator-tool] Deleting Cloudwatch Log group ${logGroupName} from ${stackName} stack`); + + try { + await throttlingBackOff(() => + cloudWatchLogsClient.send(new DeleteLogGroupCommand({ logGroupName: logGroupName })), + ); + } catch (ResourceNotFoundException) { + Logger.warn( + `[accelerator-tool] Cloudwatch Log group delete Error Log Group NOT FOUND ${logGroupName} from ${stackName} stack`, + ); + } + return true; + } + + /** + * Function to delete Cloudwatch log group + * @param backupClient + * @param stackName + * @param backupVaultName + * @private + */ + private async deleteBackupVault( + backupClient: BackupClient, + stackName: string, + backupVaultName: string, + ): Promise { + Logger.info(`[accelerator-tool] Deleting BackupVault ${backupVaultName} from ${stackName} stack`); + + try { + await throttlingBackOff(() => + backupClient.send(new DeleteBackupVaultCommand({ BackupVaultName: backupVaultName })), + ); + } catch (ResourceNotFoundException) { + Logger.warn(`[accelerator-tool] AWS BackupVault NOT FOUND ${backupVaultName} from ${stackName} stack`); + } + return true; + } + + /** + * Function to schedule Key deletion + * @param kMSClient + * @param stackName + * @param kmsKeyId + * @private + */ + private async scheduleKeyDeletion(kMSClient: KMSClient, stackName: string, kmsKeyId: string): Promise { + Logger.info(`[accelerator-tool] Disabling KMS Key ${kmsKeyId} from ${stackName} stack`); + const keyStatus = await throttlingBackOff(() => kMSClient.send(new DescribeKeyCommand({ KeyId: kmsKeyId }))); + if (keyStatus.KeyMetadata?.KeyState === KeyState.Enabled) { + await throttlingBackOff(() => kMSClient.send(new DisableKeyCommand({ KeyId: kmsKeyId }))); + + Logger.info(`[accelerator-tool] Schedule KMS Key deletion ${kmsKeyId} from ${stackName} stack`); + await throttlingBackOff(() => + kMSClient.send( + new ScheduleKeyDeletionCommand({ + KeyId: kmsKeyId, + PendingWindowInDays: 7, + }), + ), + ); + } else { + Logger.warn( + `[accelerator-tool] KMS Key ${kmsKeyId} from ${stackName} stack in ${keyStatus.KeyMetadata?.KeyState} status, can not schedule deletion`, + ); + } + return true; + } + + /** + * Function to get pipeline name from given cloudformation stack + * @param stackName + * @private + */ + private static async getPipelineNameFromCloudFormationStack( + stackName: string, + ): Promise<{ status: boolean; pipelineName: string }> { + try { + const cloudformationClient = new CloudFormationClient({}); + let nextToken: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => + cloudformationClient.send(new ListStackResourcesCommand({ StackName: stackName, NextToken: nextToken })), + ); + for (const stackResourceSummary of page.StackResourceSummaries ?? []) { + if (stackResourceSummary.ResourceType === 'AWS::CodePipeline::Pipeline') { + return { status: true, pipelineName: stackResourceSummary.PhysicalResourceId! }; + } + } + nextToken = page.NextToken; + } while (nextToken); + return { status: false, pipelineName: `No pipeline found in stack ${stackName}` }; + } catch (error) { + return { status: false, pipelineName: `${error}` }; + } + } + + /** + * Function to delete stack's resources like S3/Cloudwatch logs, KMS key + * @param cloudFormationClient + * @param stackName + * @param kMSClient + * @param cloudWatchLogsClient + * @param s3Client + * @param backupClient + * @private + */ + private async deleteStackPersistentData( + cloudFormationClient: CloudFormationClient, + stackName: string, + kMSClient: KMSClient, + cloudWatchLogsClient: CloudWatchLogsClient, + s3Client: S3Client, + backupClient: BackupClient, + ): Promise { + let nextToken: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => + cloudFormationClient.send(new ListStackResourcesCommand({ StackName: stackName, NextToken: nextToken })), + ); + for (const stackResourceSummary of page.StackResourceSummaries ?? []) { + switch (stackResourceSummary.ResourceType) { + case 'AWS::KMS::Key': + await this.scheduleKeyDeletion(kMSClient, stackName, stackResourceSummary.PhysicalResourceId!); + break; + case 'AWS::Backup::BackupVault': + await this.deleteBackupVault(backupClient, stackName, stackResourceSummary.PhysicalResourceId!); + break; + case 'AWS::Logs::LogGroup': + await this.deleteCloudWatchLogs(cloudWatchLogsClient, stackName, stackResourceSummary.PhysicalResourceId!); + break; + case 'AWS::S3::Bucket': + const listBucketResponse = await throttlingBackOff(() => s3Client.send(new ListBucketsCommand({}))); + for (const bucket of listBucketResponse.Buckets!) { + if (bucket.Name === stackResourceSummary.PhysicalResourceId) { + await this.deleteBucket(s3Client, stackName, stackResourceSummary.PhysicalResourceId!); + } + } + break; + } + } + nextToken = page.NextToken; + } while (nextToken); + return true; + } + + /** + * Function to create list of stack resources for S3, KMS, Cloudwatch logs, which will be deleted after stack deletion completed. + * @param cloudFormationClient + * @param stackName + * @param kMSClient + * @param cloudWatchLogsClient + * @param s3Client + * @private + */ + private async makeStackPersistentObjectList( + cloudFormationClient: CloudFormationClient, + stackName: string, + kMSClient: KMSClient, + cloudWatchLogsClient: CloudWatchLogsClient, + s3Client: S3Client, + ): Promise { + const stackPersistentObjectList: stackPersistentObjectListType[] = []; + let nextToken: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => + cloudFormationClient.send(new ListStackResourcesCommand({ StackName: stackName, NextToken: nextToken })), + ); + for (const stackResourceSummary of page.StackResourceSummaries ?? []) { + switch (stackResourceSummary.ResourceType) { + case 'AWS::KMS::Key': + stackPersistentObjectList.push({ + stackName: stackName, + resourceType: 'KMS', + resourceClient: kMSClient, + resourcePhysicalId: stackResourceSummary.PhysicalResourceId!, + }); + break; + case 'AWS::Logs::LogGroup': + stackPersistentObjectList.push({ + stackName: stackName, + resourceType: 'CWLogs', + resourceClient: cloudWatchLogsClient, + resourcePhysicalId: stackResourceSummary.PhysicalResourceId!, + }); + break; + case 'AWS::S3::Bucket': + const listBucketResponse = await throttlingBackOff(() => s3Client.send(new ListBucketsCommand({}))); + for (const bucket of listBucketResponse.Buckets!) { + if (bucket.Name === stackResourceSummary.PhysicalResourceId) { + stackPersistentObjectList.push({ + stackName: stackName, + resourceType: 'S3', + resourceClient: s3Client, + resourcePhysicalId: stackResourceSummary.PhysicalResourceId!, + }); + } + } + break; + } + } + nextToken = page.NextToken; + } while (nextToken); + return stackPersistentObjectList; + } + + /** + * Function to check if given stack deletion completed. + * @param cloudFormationClient + * @param stackName + * @private + */ + private async isStackDeletionCompleted( + cloudFormationClient: CloudFormationClient, + stackName: string, + ): Promise { + try { + await throttlingBackOff(() => cloudFormationClient.send(new DescribeStacksCommand({ StackName: stackName }))); + return false; + } catch (error) { + return `${error}`.includes(`Stack with id ${stackName} does not exist`); + } + } + + /** + * Function to delete cloudformation stack + * @param accountStack + * @private + */ + private async deleteStacks(accountStack: { + deleteOrder: number; + resourceDetails: [{ accountId: string; stackName: string }]; + }): Promise { + const deleteStackStartedPromises: Promise[] = []; + const deleteStackCompletedPromises: Promise[] = []; + const assumeRoleName = 'AWSControlTowerExecution'; + let cloudFormationClient: CloudFormationClient; + let s3Client: S3Client; + let kMSClient: KMSClient; + let cloudWatchLogsClient: CloudWatchLogsClient; + let backupClient: BackupClient; + + let cloudFormationStack: Stack | undefined; + //Use a loop for all regions + for (const region of this.globalConfig!.enabledRegions) { + for (const resource of accountStack.resourceDetails) { + if (resource.accountId !== this.pipelineManagementAccount?.accountId) { + const roleArn = `arn:${this.acceleratorToolProps.partition}:iam::${resource.accountId}:role/${assumeRoleName}`; + const stsClient = new STSClient({ region: region }); + const assumeRoleCredential = await throttlingBackOff(() => + stsClient.send( + new AssumeRoleCommand({ + RoleArn: roleArn, + RoleSessionName: 'acceleratorAssumeRoleSession', + }), + ), + ); + cloudFormationClient = new CloudFormationClient({ + region: region, + credentials: { + accessKeyId: assumeRoleCredential.Credentials!.AccessKeyId!, + secretAccessKey: assumeRoleCredential.Credentials!.SecretAccessKey!, + sessionToken: assumeRoleCredential.Credentials!.SessionToken, + expiration: assumeRoleCredential.Credentials!.Expiration, + }, + }); + s3Client = new S3Client({ + region: region, + credentials: { + accessKeyId: assumeRoleCredential.Credentials!.AccessKeyId!, + secretAccessKey: assumeRoleCredential.Credentials!.SecretAccessKey!, + sessionToken: assumeRoleCredential.Credentials!.SessionToken, + expiration: assumeRoleCredential.Credentials!.Expiration, + }, + }); + kMSClient = new KMSClient({ + region: region, + credentials: { + accessKeyId: assumeRoleCredential.Credentials!.AccessKeyId!, + secretAccessKey: assumeRoleCredential.Credentials!.SecretAccessKey!, + sessionToken: assumeRoleCredential.Credentials!.SessionToken, + expiration: assumeRoleCredential.Credentials!.Expiration, + }, + }); + cloudWatchLogsClient = new CloudWatchLogsClient({ + region: region, + credentials: { + accessKeyId: assumeRoleCredential.Credentials!.AccessKeyId!, + secretAccessKey: assumeRoleCredential.Credentials!.SecretAccessKey!, + sessionToken: assumeRoleCredential.Credentials!.SessionToken, + expiration: assumeRoleCredential.Credentials!.Expiration, + }, + }); + backupClient = new BackupClient({ + region: region, + credentials: { + accessKeyId: assumeRoleCredential.Credentials!.AccessKeyId!, + secretAccessKey: assumeRoleCredential.Credentials!.SecretAccessKey!, + sessionToken: assumeRoleCredential.Credentials!.SessionToken, + expiration: assumeRoleCredential.Credentials!.Expiration, + }, + }); + } else { + cloudFormationClient = new CloudFormationClient({ region: region }); + s3Client = new S3Client({ region: region }); + kMSClient = new KMSClient({ region: region }); + cloudWatchLogsClient = new CloudWatchLogsClient({ region: region }); + backupClient = new BackupClient({ region: region }); + } + + const fullyQualifiedStackName = `${resource.stackName}-${resource.accountId}-${region}`; + + try { + const response = await throttlingBackOff(() => + cloudFormationClient.send(new DescribeStacksCommand({ StackName: fullyQualifiedStackName })), + ); + cloudFormationStack = response.Stacks![0]; + } catch (error) { + if (`${error}`.includes(`Stack with id ${fullyQualifiedStackName} does not exist`)) { + Logger.warn( + `[accelerator-tool] Stack with id ${fullyQualifiedStackName} does not exist in ${resource.accountId} account in ${region} region`, + ); + cloudFormationStack = undefined; + } + } + + // Exclude management account home region bootstrap deletion before pipeline stack and installer stacks are deleted, conditions are + // 1. When pipeline account is not used + // 2. When stack is for home region + // 3. When it is bootstrap stack + // 4. When stack is part of management account + if ( + cloudFormationStack && + !this.externalPipelineAccount.isUsed && + this.globalConfig?.homeRegion === region && + cloudFormationStack?.StackName === 'AWSAccelerator-CDKToolkit' && + this.pipelineManagementAccount!.accountId === resource.accountId && + !this.acceleratorToolProps.keepBootstraps + ) { + Logger.info(`[accelerator-tool] Management account home region bootstrap stack deletion excluded`); + Logger.info( + `[accelerator-tool] ${cloudFormationStack?.StackName} stack region is ${region} and home region is ${this.globalConfig?.homeRegion}`, + ); + + cloudFormationStack = undefined; + } + + if ( + cloudFormationStack && + (await this.isStackDeletable( + cloudFormationClient, + cloudFormationStack.StackName!, + resource.accountId, + region, + )) + ) { + Logger.info( + `[accelerator-tool] Deleting CloudFormation stack ${cloudFormationStack!.StackName} in ${ + resource.accountId + } account from ${region} region`, + ); + + // If delete-data flag is on perform deletion, before stack deletion + if (this.acceleratorToolProps.deleteData) { + Logger.info('[accelerator-tool] delete-data flag is ON !!!'); + + // Since prepare stack have dependencies on KSM key, can't delete resources before stacks deleted. + if (cloudFormationStack!.StackName!.includes('PrepareStack')) { + this.prepareStackPersistentObjectList = await this.makeStackPersistentObjectList( + cloudFormationClient, + cloudFormationStack!.StackName!, + kMSClient, + cloudWatchLogsClient, + s3Client, + ); + } else { + await this.deleteStackPersistentData( + // this.stackPersistentObjectList = await this.makeStackPersistentObjectList( + cloudFormationClient, + cloudFormationStack!.StackName!, + kMSClient, + cloudWatchLogsClient, + s3Client, + backupClient, + ); + } + } else { + Logger.info('[accelerator-tool] delete-data flag is OFF'); + } + + deleteStackStartedPromises.push( + cloudFormationClient.send(new DeleteStackCommand({ StackName: cloudFormationStack!.StackName! })), + ); + + // This is required to make sure stacks are deleted before moving to next stage of stack deletion + deleteStackCompletedPromises.push( + waitUntilStackDeleteComplete( + { client: cloudFormationClient, maxWaitTime: 3600 }, // waitTime is in second + { StackName: cloudFormationStack!.StackName! }, + ), + ); + } + } + } + + if (deleteStackStartedPromises.length > 0 && deleteStackCompletedPromises.length > 0) { + Logger.info(`[accelerator-tool] Total ${deleteStackStartedPromises.length} stack(s) will be deleted.`); + const start = new Date().getTime(); + await Promise.all(deleteStackStartedPromises); + await Promise.all(deleteStackCompletedPromises); + const elapsed = Math.round((new Date().getTime() - start) / 60000); + Logger.info( + `[accelerator-tool] Total ${deleteStackCompletedPromises.length} stack(s) deleted successfully. Elapsed time - ~${elapsed} minutes`, + ); + } + return true; + } + + /** + * Function to see is cloudformation stack is deletable + * If stack termination protection is ON and uninstaller flag to ignore termination protection is on then it is considered to be deletable + * @param cloudFormationClient + * @param stackName + * @param accountId + * @param region + * @private + */ + private async isStackDeletable( + cloudFormationClient: CloudFormationClient, + stackName: string, + accountId?: string, + region?: string, + ): Promise { + let enableTerminationProtection = false; + const response = await throttlingBackOff(() => + cloudFormationClient.send(new DescribeStacksCommand({ StackName: stackName })), + ); + enableTerminationProtection = response.Stacks![0].EnableTerminationProtection ?? false; + if (enableTerminationProtection) { + if (this.acceleratorToolProps.ignoreTerminationProtection) { + Logger.warn( + `[accelerator-tool] Stack ${stackName} termination protection is enabled, disabling the termination protection"`, + ); + await throttlingBackOff(() => + cloudFormationClient.send( + new UpdateTerminationProtectionCommand({ + StackName: stackName, + EnableTerminationProtection: false, + }), + ), + ); + Logger.warn(`[accelerator-tool] Waiting stack ${stackName} update completion"`); + waitUntilStackUpdateComplete( + { client: cloudFormationClient, maxWaitTime: 3600 }, // waitTime is in second + { StackName: stackName }, + ); + return true; + } else { + if (stackName && accountId) { + Logger.warn( + `[accelerator-tool] Due to termination protection enable skipping deletion of CloudFormation stack ${stackName} in ${accountId} account from ${region} region`, + ); + } else { + Logger.warn( + `[accelerator-tool] Due to termination protection enable skipping deletion of CloudFormation stack ${stackName}`, + ); + } + Logger.warn( + `[accelerator-tool] Uninstallation STOPPED, due to termination protection of stack ${stackName}!!!"`, + ); + process.abort(); + return false; + } + } else { + return true; + } + } + + /** + * Function to delete accelerator tester stack + * @param stackNamePrefix + * @private + */ + private async deleteTesterStack(stackNamePrefix: string): Promise { + const cloudFormationClient = new CloudFormationClient({}); + const testerStackName = `${stackNamePrefix}-${ + this.externalPipelineAccount.isUsed + ? this.externalPipelineAccount.accountId! + : this.pipelineManagementAccount!.accountId + }-${this.globalConfig?.homeRegion}`; + + await this.deleteStack(cloudFormationClient, testerStackName); + return true; + } + + /** + * Function to delete accelerator tester pipeline stack + * @param testerPipelineStackNamePrefix + * @param testerPipelineConfigRepositoryName + * @private + */ + private async deleteTesterPipelineStack( + testerPipelineStackNamePrefix: string, + testerPipelineConfigRepositoryName: string, + ): Promise { + const cloudFormationClient = new CloudFormationClient({}); + + if (!this.acceleratorToolProps.deletePipelines) { + return true; + } + + const testerPipelineStackName = `${testerPipelineStackNamePrefix}-${ + this.externalPipelineAccount.isUsed + ? this.externalPipelineAccount.accountId! + : this.pipelineManagementAccount!.accountId + }-${this.globalConfig?.homeRegion}`; + + if (this.acceleratorToolProps.deleteConfigRepo) { + await this.deleteCodecommitRepository(new CodeCommitClient({}), testerPipelineConfigRepositoryName); + } + + const testerPipeline = await AcceleratorTool.getPipelineNameFromCloudFormationStack(testerPipelineStackName); + + if (testerPipeline.status) { + const codeBuildClient = new CodeBuildClient({}); + const response = await throttlingBackOff(() => + codeBuildClient.send(new BatchGetProjectsCommand({ names: [testerPipeline.pipelineName] })), + ); + for (const project of response.projects ?? []) { + await this.deleteCodeBuilds(codeBuildClient, project.name!); + } + } + + await this.deleteStack(cloudFormationClient, testerPipelineStackName); + + return true; + } + + /** + * Function to delete cloudformation stack + * @param cloudFormationClient + * @param stackName + * @private + */ + private async deleteStack(cloudFormationClient: CloudFormationClient, stackName: string): Promise { + try { + await throttlingBackOff(() => cloudFormationClient.send(new DescribeStacksCommand({ StackName: stackName }))); + // cloudFormationStack = response.Stacks![0].StackName; + } catch (error) { + if (`${error}`.includes(`Stack with id ${stackName} does not exist`)) { + Logger.warn(`[accelerator-tool] Stack with id ${stackName} does not exist`); + return true; + } + } + + if (!(await this.isStackDeletable(cloudFormationClient, stackName))) { + return true; + } + + if (this.acceleratorToolProps.deleteData) { + // Delete Installer stack persistent data + await this.deleteStackPersistentData( + cloudFormationClient, + stackName, + new KMSClient({}), + new CloudWatchLogsClient({}), + new S3Client({}), + new BackupClient({}), + ); + } + + Logger.info(`[accelerator-tool] Deleting stack ${stackName}`); + await throttlingBackOff(() => cloudFormationClient.send(new DeleteStackCommand({ StackName: `${stackName}` }))); + + Logger.info(`[accelerator-tool] Waiting until stack ${stackName} deletion completes`); + while (!(await this.isStackDeletionCompleted(cloudFormationClient, stackName))) { + // Wait before checking again + await new Promise(func => setTimeout(func, 1000)); + } + Logger.info(`[accelerator-tool] Stack ${stackName} deleted successfully`); + return true; + } + + /** + * Function to delete code commit repository + * @param codeCommitClient + * @param repositoryName + * @private + */ + private async deleteCodecommitRepository( + codeCommitClient: CodeCommitClient, + repositoryName: string, + ): Promise { + //Delete config repository + Logger.info(`[accelerator-tool] Deleting code commit repository ${repositoryName}`); + await throttlingBackOff(() => + codeCommitClient.send(new DeleteRepositoryCommand({ repositoryName: repositoryName })), + ); + + return true; + } + + /** + * Delete prepare stack resources + * @private + */ + private async deletePrepareStackResources(): Promise { + // Now delete persistent resources from deleted stacks + Logger.info(`[accelerator-tool] Deleting S3, cloudwatch logs and KMS keys`); + for (const stackPersistentObject of this.prepareStackPersistentObjectList) { + switch (stackPersistentObject.resourceType) { + case 'KMS': + // await this.scheduleKeyDeletion(kMSClient, stackName, stackResourceSummary.PhysicalResourceId!); + await this.scheduleKeyDeletion( + stackPersistentObject.resourceClient as KMSClient, + stackPersistentObject.stackName, + stackPersistentObject.resourcePhysicalId, + ); + break; + case 'CWLogs': + await this.deleteCloudWatchLogs( + stackPersistentObject.resourceClient as CloudWatchLogsClient, + stackPersistentObject.stackName, + stackPersistentObject.resourcePhysicalId, + ); + break; + case 'S3': + await this.deleteBucket( + stackPersistentObject.resourceClient as S3Client, + stackPersistentObject.stackName, + stackPersistentObject.resourcePhysicalId, + ); + break; + } + } + + return true; + } + + /** + * Function to delete build ids for code build project + * @param codeBuildClient + * @param buildProjectName + * @private + */ + private async deleteCodeBuilds(codeBuildClient: CodeBuildClient, buildProjectName: string): Promise { + //delete code build projects before deleting pipeline + const buildIds: string[] = []; + let nextToken: string | undefined = undefined; + do { + const page = await throttlingBackOff(() => + codeBuildClient.send(new ListBuildsForProjectCommand({ projectName: buildProjectName, nextToken })), + ); + for (const id of page.ids!) { + buildIds.push(id); + } + nextToken = page.nextToken; + } while (nextToken); + Logger.info(`[accelerator-tool] Deleting build ids for the project ${buildProjectName}`); + await throttlingBackOff(() => codeBuildClient.send(new BatchDeleteBuildsCommand({ ids: buildIds }))); + return true; + } +} diff --git a/source/packages/@aws-accelerator/tools/package.json b/source/packages/@aws-accelerator/tools/package.json new file mode 100644 index 000000000..3f7d194c4 --- /dev/null +++ b/source/packages/@aws-accelerator/tools/package.json @@ -0,0 +1,58 @@ +{ + "name": "@aws-accelerator/tools", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "tsc", + "watch": "tsc -w", + "test": "jest --coverage --ci", + "lint": "eslint --fix --max-warnings 0 -c ../../../.eslintrc.json 'lib/**/*.{ts,tsx}' 'test/**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../.eslintrc.json 'lib/**/*.{ts,tsx}' 'test/**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "@typescript-eslint/eslint-plugin": "5.6.0", + "@typescript-eslint/parser": "5.6.0", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "ts-node": "10.4.0", + "typescript": "4.5.2" + }, + "peerDependencies": { + "eslint": "8.4.1" + }, + "dependencies": { + "@aws-accelerator/config": "^0.0.0", + "@aws-accelerator/utils": "^0.0.0", + "@aws-cdk/cx-api": "2.16.0", + "@aws-sdk/client-backup": "3.92.0", + "@aws-sdk/client-cloudformation": "3.92.0", + "@aws-sdk/client-cloudwatch-logs": "3.92.0", + "@aws-sdk/client-codebuild": "3.92.0", + "@aws-sdk/client-codecommit": "3.92.0", + "@aws-sdk/client-codepipeline": "3.92.0", + "@aws-sdk/client-kms": "3.92.0", + "@aws-sdk/client-organizations": "3.92.0", + "@aws-sdk/client-s3": "3.92.0" + } +} diff --git a/source/packages/@aws-accelerator/tools/test/uninstaller.test.ts b/source/packages/@aws-accelerator/tools/test/uninstaller.test.ts new file mode 100644 index 000000000..f2667a2f5 --- /dev/null +++ b/source/packages/@aws-accelerator/tools/test/uninstaller.test.ts @@ -0,0 +1,22 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +test('Uninstaller test', () => { + // // const app = new cdk.App(); + // // // WHEN + // // const stack = new Installer.InstallerStack(app, 'MyTestStack'); + // // // THEN + // // // expectCDK(stack).to(matchTemplate({ + // // // "Resources": {} + // // // }, MatchStyle.EXACT)) +}); diff --git a/source/packages/@aws-accelerator/tools/tsconfig.json b/source/packages/@aws-accelerator/tools/tsconfig.json new file mode 100644 index 000000000..cdfe7384b --- /dev/null +++ b/source/packages/@aws-accelerator/tools/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "lib": ["dom"] + }, + "include": ["lib/**/*", "index.ts", "uninstaller.ts"] +} \ No newline at end of file diff --git a/source/packages/@aws-accelerator/tools/uninstaller.ts b/source/packages/@aws-accelerator/tools/uninstaller.ts new file mode 100644 index 000000000..2dcf29049 --- /dev/null +++ b/source/packages/@aws-accelerator/tools/uninstaller.ts @@ -0,0 +1,80 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +import { Logger } from '../accelerator/lib/logger'; +// import { Logger } from '@aws-accelerator/accelerator'; +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; +import { AcceleratorTool } from './lib/classes/accelerator-tool'; +/** + * AWS Accelerator Uninstaller tool entry point. + * Script Options: + *
    + *
  • --installerStackName The name of the installer cloudformation stack + *
  • --keepBootstraps(optional) When this flag used CDK bootstrap stacks will be deleted + *
  • --deleteData(optional) When this flag used S3 buckets, cloudwatch log groups etc. will be deleted. + *
  • --deleteConfigRepo(optional) When this flag used configuration repository will be deleted. + *
  • --deletePipelines(optional) When this flag used pipelined will be deleted. + *
  • --ignoreTerminationProtection(optional) When this flag used termination protected stacks will be deleted. + *
+ * @example + * ts-node uninstaller.ts --installer-stack-name --keep-bootstraps --delete-data --delete-pipelines + */ +async function main(): Promise { + const start = new Date().getTime(); + const usage = + '** Script Usage ** ts-node uninstaller.ts --installerStackName [--partition] [--keepBootstraps] [--deleteData] [--deleteConfigRepo] [--deletePipelines] [--ignoreTerminationProtection]'; + + const argv = yargs(hideBin(process.argv)).argv; + const installerStackName = argv['installerStackName'] as string; + if (installerStackName === undefined) { + Logger.warn(`[uninstaller] Invalid --installerStackName ${installerStackName}`); + throw new Error(usage); + } + + const partition = (argv['partition'] as string) ?? 'aws'; + const keepBootstraps = (argv['keepBootstraps'] as boolean) ?? false; + const deleteData = (argv['deleteData'] as boolean) ?? false; + const deleteConfigRepo = (argv['deleteConfigRepo'] as boolean) ?? false; + const deletePipelines = (argv['deletePipelines'] as boolean) ?? false; + const ignoreTerminationProtection = (argv['ignoreTerminationProtection'] as boolean) ?? false; + + const acceleratorTool = new AcceleratorTool({ + installerStackName: installerStackName, + partition: partition, + keepBootstraps: keepBootstraps, + deleteData: deleteData, + deleteConfigRepo: deleteConfigRepo, + deletePipelines: deletePipelines, + ignoreTerminationProtection: ignoreTerminationProtection, + }); + + const status = await acceleratorTool.uninstallAccelerator(installerStackName); + const elapsed = Math.round((new Date().getTime() - start) / 60000); + + return status + ? `[uninstaller] Uninstallation completed successfully for installer stack "${installerStackName}". Elapsed time ~${elapsed} minutes` + : `[uninstaller] Uninstallation failed for installer stack "${installerStackName}". Elapsed time ~${elapsed} minutes`; +} + +process.on('unhandledRejection', (reason, _) => { + console.error(reason); + // eslint-disable-next-line no-process-exit + process.exit(1); +}); + +/** + * Call Main function + */ +main().then(data => { + Logger.info(data); +}); diff --git a/source/packages/@aws-accelerator/utils/README.md b/source/packages/@aws-accelerator/utils/README.md new file mode 100644 index 000000000..8ead5bd0f --- /dev/null +++ b/source/packages/@aws-accelerator/utils/README.md @@ -0,0 +1 @@ +# @aws-accelerator/utils diff --git a/source/packages/@aws-accelerator/utils/index.ts b/source/packages/@aws-accelerator/utils/index.ts new file mode 100644 index 000000000..c11d77ae2 --- /dev/null +++ b/source/packages/@aws-accelerator/utils/index.ts @@ -0,0 +1 @@ +export * from './lib/throttle'; diff --git a/source/packages/@aws-accelerator/utils/lib/throttle.ts b/source/packages/@aws-accelerator/utils/lib/throttle.ts new file mode 100644 index 000000000..26c68e73f --- /dev/null +++ b/source/packages/@aws-accelerator/utils/lib/throttle.ts @@ -0,0 +1,64 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { backOff, IBackOffOptions } from 'exponential-backoff'; + +/** + * Auxiliary function to retry AWS SDK calls when a throttling error occurs. + */ +export function throttlingBackOff( + request: () => Promise, + options?: Partial>, +): Promise { + return backOff(request, { + startingDelay: 500, + jitter: 'full', + retry: isThrottlingError, + ...options, + }); +} + +export const isThrottlingError = ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/explicit-module-boundary-types + e: any, +): boolean => + e.retryable === true || + // SDKv2 Error Structure + e.code === 'ConcurrentModificationException' || // Retry for AWS Organizations + e.code === 'InsufficientDeliveryPolicyException' || // Retry for ConfigService + e.code === 'NoAvailableDeliveryChannelException' || // Retry for ConfigService + e.code === 'ConcurrentModifications' || // Retry for AssociateHostedZone + e.code === 'LimitExceededException' || // Retry for SecurityHub + e.code === 'OperationNotPermittedException' || // Retry for RAM + e.code === 'TooManyRequestsException' || + e.code === 'Throttling' || + e.code === 'ThrottlingException' || + e.code === 'InternalErrorException' || + e.code === 'InternalException' || + // SDKv3 Error Structure + e.name === 'ConcurrentModificationException' || // Retry for AWS Organizations + e.name === 'InsufficientDeliveryPolicyException' || // Retry for ConfigService + e.name === 'NoAvailableDeliveryChannelException' || // Retry for ConfigService + e.name === 'ConcurrentModifications' || // Retry for AssociateHostedZone + e.name === 'LimitExceededException' || // Retry for SecurityHub + e.name === 'OperationNotPermittedException' || // Retry for RAM + e.name === 'TooManyRequestsException' || + e.name === 'Throttling' || + e.name === 'ThrottlingException' || + e.name === 'InternalErrorException' || + e.name === 'InternalException'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export async function delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} diff --git a/source/packages/@aws-accelerator/utils/package.json b/source/packages/@aws-accelerator/utils/package.json new file mode 100644 index 000000000..2435db292 --- /dev/null +++ b/source/packages/@aws-accelerator/utils/package.json @@ -0,0 +1,45 @@ +{ + "name": "@aws-accelerator/utils", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "tsc", + "watch": "tsc -w", + "test": "jest --coverage --ci", + "lint": "eslint --fix --max-warnings 0 -c ../../../.eslintrc.json 'lib/**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../.eslintrc.json 'lib/**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2" + }, + "dependencies": { + "exponential-backoff": "3.1.0" + }, + "nohoist": [ + "**/exponential-backoff", + "**/exponential-backoff/**" + ] +} diff --git a/source/packages/@aws-accelerator/utils/test/throttle.test.ts b/source/packages/@aws-accelerator/utils/test/throttle.test.ts new file mode 100644 index 000000000..6f06d5920 --- /dev/null +++ b/source/packages/@aws-accelerator/utils/test/throttle.test.ts @@ -0,0 +1,25 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// import { countResources, expect as expectCDK } from '@aws-cdk/assert'; +// import * as cdk from 'aws-cdk-lib'; + +test('Throttle test', () => { + // // const app = new cdk.App(); + // // // WHEN + // // const stack = new Installer.InstallerStack(app, 'MyTestStack'); + // // // THEN + // // // expectCDK(stack).to(matchTemplate({ + // // // "Resources": {} + // // // }, MatchStyle.EXACT)) +}); diff --git a/source/packages/@aws-accelerator/utils/tsconfig.json b/source/packages/@aws-accelerator/utils/tsconfig.json new file mode 100644 index 000000000..fc62a1e09 --- /dev/null +++ b/source/packages/@aws-accelerator/utils/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["lib/**/*", "index.ts"], + "exclude": ["test/**/*"] +} diff --git a/source/packages/@aws-cdk-extensions/cdk-extensions/.gitignore b/source/packages/@aws-cdk-extensions/cdk-extensions/.gitignore new file mode 100644 index 000000000..f60797b6a --- /dev/null +++ b/source/packages/@aws-cdk-extensions/cdk-extensions/.gitignore @@ -0,0 +1,8 @@ +*.js +!jest.config.js +*.d.ts +node_modules + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/source/packages/@aws-cdk-extensions/cdk-extensions/.npmignore b/source/packages/@aws-cdk-extensions/cdk-extensions/.npmignore new file mode 100644 index 000000000..c1d6d45dc --- /dev/null +++ b/source/packages/@aws-cdk-extensions/cdk-extensions/.npmignore @@ -0,0 +1,6 @@ +*.ts +!*.d.ts + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/source/packages/@aws-cdk-extensions/cdk-extensions/README.md b/source/packages/@aws-cdk-extensions/cdk-extensions/README.md new file mode 100644 index 000000000..e09375bed --- /dev/null +++ b/source/packages/@aws-cdk-extensions/cdk-extensions/README.md @@ -0,0 +1 @@ +# @aws-cdk-extensions/cdk-extensions diff --git a/source/packages/@aws-cdk-extensions/cdk-extensions/index.ts b/source/packages/@aws-cdk-extensions/cdk-extensions/index.ts new file mode 100644 index 000000000..7319eecb5 --- /dev/null +++ b/source/packages/@aws-cdk-extensions/cdk-extensions/index.ts @@ -0,0 +1,15 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +export * from './lib/repository'; +export * from './lib/trail'; diff --git a/source/packages/@aws-cdk-extensions/cdk-extensions/jest.config.js b/source/packages/@aws-cdk-extensions/cdk-extensions/jest.config.js new file mode 100644 index 000000000..0e8a6842d --- /dev/null +++ b/source/packages/@aws-cdk-extensions/cdk-extensions/jest.config.js @@ -0,0 +1,6 @@ +const base = require('../../../jest.config.base'); +const packageJson = require('./package.json'); + +module.exports = { + ...base.getJestJunitConfig(packageJson.name), +}; diff --git a/source/packages/@aws-cdk-extensions/cdk-extensions/lib/repository.ts b/source/packages/@aws-cdk-extensions/cdk-extensions/lib/repository.ts new file mode 100644 index 000000000..f9b6a573b --- /dev/null +++ b/source/packages/@aws-cdk-extensions/cdk-extensions/lib/repository.ts @@ -0,0 +1,64 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { Construct } from 'constructs'; +import * as codecommit from 'aws-cdk-lib/aws-codecommit'; + +/** + * Initialized Repository properties + */ +export interface RepositoryProps extends codecommit.RepositoryProps { + /** + * Name of the repository. + * + * This property contains s3 bucket name to initialize CodeCommit repositories. + * + * + */ + readonly s3BucketName: string; + /** + * Name of the repository. + * + * This property contains s3 object key for initializing CodeCommit repositories. + * + * + */ + readonly s3key: string; + /** + * A branch name of the repository to be initialized. + * + * This is an optional property + * + * @default - main + */ + readonly repositoryBranchName?: string; +} + +/** + * Class to initialize repository + */ +export class Repository extends codecommit.Repository { + constructor(scope: Construct, id: string, props: RepositoryProps) { + super(scope, id, props); + + const cfnRepository = this.node.defaultChild as codecommit.CfnRepository; + + cfnRepository.code = { + branchName: props.repositoryBranchName ? props.repositoryBranchName : 'main', + s3: { + bucket: props.s3BucketName, + key: props.s3key, + }, + }; + } +} diff --git a/source/packages/@aws-cdk-extensions/cdk-extensions/lib/trail.ts b/source/packages/@aws-cdk-extensions/cdk-extensions/lib/trail.ts new file mode 100644 index 000000000..a836af97b --- /dev/null +++ b/source/packages/@aws-cdk-extensions/cdk-extensions/lib/trail.ts @@ -0,0 +1,28 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cloudtrail from 'aws-cdk-lib/aws-cloudtrail'; +import { Construct } from 'constructs'; + +export interface TrailProps extends cloudtrail.TrailProps { + readonly isOrganizationTrail: boolean; +} + +export class Trail extends cloudtrail.Trail { + constructor(scope: Construct, id: string, props: TrailProps) { + super(scope, id, props); + + const cfnRepository = this.node.defaultChild as cloudtrail.CfnTrail; + cfnRepository.isOrganizationTrail = props.isOrganizationTrail; + } +} diff --git a/source/packages/@aws-cdk-extensions/cdk-extensions/package.json b/source/packages/@aws-cdk-extensions/cdk-extensions/package.json new file mode 100644 index 000000000..dc41ab09b --- /dev/null +++ b/source/packages/@aws-cdk-extensions/cdk-extensions/package.json @@ -0,0 +1,48 @@ +{ + "name": "@aws-cdk-extensions/cdk-extensions", + "version": "0.0.0", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist && rm -rf cdk.out", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "tsc", + "watch": "tsc -w", + "test": "jest --coverage --ci", + "lint": "eslint --fix --max-warnings 0 -c ../../../.eslintrc.json 'lib/**/*.{ts,tsx}' 'test/**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" ", + "precommit": "eslint --max-warnings 0 -c ../../../.eslintrc.json 'lib/**/*.{ts,tsx}' 'test/**/*.{ts,tsx}' --ignore-pattern \"*.d.ts\" " + }, + "devDependencies": { + "@aws-cdk/assert": "2.16.0", + "aws-cdk-lib": "2.16.0", + "constructs": "10.0.12", + "@types/jest": "27.0.3", + "@types/node": "16.11.12", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-import-resolver-node": "0.3.6", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-license-header": "0.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "27.4.3", + "prettier": "2.5.1", + "ts-jest": "27.1.1", + "typescript": "4.5.2" + }, + "peerDependencies": { + "aws-cdk-lib": "2.16.0", + "constructs": "10.0.12" + }, + "dependencies": { + "aws-cdk-lib": "2.16.0" + } +} diff --git a/source/packages/@aws-cdk-extensions/cdk-extensions/test/__snapshots__/repository-snapshot.test.ts.snap b/source/packages/@aws-cdk-extensions/cdk-extensions/test/__snapshots__/repository-snapshot.test.ts.snap new file mode 100644 index 000000000..2644316b0 --- /dev/null +++ b/source/packages/@aws-cdk-extensions/cdk-extensions/test/__snapshots__/repository-snapshot.test.ts.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Initialized CodeCommit Repository Snapshot Test 1`] = ` +Object { + "Resources": Object { + "SnapshotTest4B1F0CC8": Object { + "Properties": Object { + "Code": Object { + "BranchName": "main", + "S3": Object { + "Bucket": "Testbucket", + "Key": "testkey", + }, + }, + "RepositoryName": "AWS-accelerator", + }, + "Type": "AWS::CodeCommit::Repository", + }, + }, +} +`; diff --git a/source/packages/@aws-cdk-extensions/cdk-extensions/test/__snapshots__/repository.test.ts.snap b/source/packages/@aws-cdk-extensions/cdk-extensions/test/__snapshots__/repository.test.ts.snap new file mode 100644 index 000000000..2644316b0 --- /dev/null +++ b/source/packages/@aws-cdk-extensions/cdk-extensions/test/__snapshots__/repository.test.ts.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Initialized CodeCommit Repository Snapshot Test 1`] = ` +Object { + "Resources": Object { + "SnapshotTest4B1F0CC8": Object { + "Properties": Object { + "Code": Object { + "BranchName": "main", + "S3": Object { + "Bucket": "Testbucket", + "Key": "testkey", + }, + }, + "RepositoryName": "AWS-accelerator", + }, + "Type": "AWS::CodeCommit::Repository", + }, + }, +} +`; diff --git a/source/packages/@aws-cdk-extensions/cdk-extensions/test/repository-fine-grained.test.ts b/source/packages/@aws-cdk-extensions/cdk-extensions/test/repository-fine-grained.test.ts new file mode 100644 index 000000000..09960098c --- /dev/null +++ b/source/packages/@aws-cdk-extensions/cdk-extensions/test/repository-fine-grained.test.ts @@ -0,0 +1,36 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { expect as expectCDK, haveResourceLike } from '@aws-cdk/assert'; +import { test, describe } from '@jest/globals'; +import * as TestConfig from './test-config'; +import * as CdkExtensions from '../index'; + +describe('Initialized CodeCommit Repository', () => { + test('Initialization Properties Test', () => { + new CdkExtensions.Repository(TestConfig.stack, 'SnapshotTest', TestConfig.repositoryProps); + + expectCDK(TestConfig.stack).to( + haveResourceLike('AWS::CodeCommit::Repository', { + RepositoryName: TestConfig.repositoryProps.repositoryName, + Code: { + BranchName: TestConfig.repositoryProps.repositoryBranchName, + S3: { + Bucket: TestConfig.repositoryProps.s3BucketName, + Key: TestConfig.repositoryProps.s3key, + }, + }, + }), + ); + }); +}); diff --git a/source/packages/@aws-cdk-extensions/cdk-extensions/test/repository-snapshot.test.ts b/source/packages/@aws-cdk-extensions/cdk-extensions/test/repository-snapshot.test.ts new file mode 100644 index 000000000..f5b260e82 --- /dev/null +++ b/source/packages/@aws-cdk-extensions/cdk-extensions/test/repository-snapshot.test.ts @@ -0,0 +1,27 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { SynthUtils } from '@aws-cdk/assert'; +import { test, describe, expect } from '@jest/globals'; +import * as CdkExtensions from '../index'; +import * as TestConfig from './test-config'; + +describe('Initialized CodeCommit Repository', () => { + /** + * Snapshot Test - Initialzed Repository + */ + test('Snapshot Test', () => { + new CdkExtensions.Repository(TestConfig.stack, 'SnapshotTest', TestConfig.repositoryProps); + expect(SynthUtils.toCloudFormation(TestConfig.stack)).toMatchSnapshot(); + }); +}); diff --git a/source/packages/@aws-cdk-extensions/cdk-extensions/test/repository.test.ts b/source/packages/@aws-cdk-extensions/cdk-extensions/test/repository.test.ts new file mode 100644 index 000000000..050d0de35 --- /dev/null +++ b/source/packages/@aws-cdk-extensions/cdk-extensions/test/repository.test.ts @@ -0,0 +1,62 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { expect as expectCDK, haveResource, SynthUtils } from '@aws-cdk/assert'; +import * as cdk from 'aws-cdk-lib'; +import * as CdkExtensions from '../index'; +import { test, describe, expect } from '@jest/globals'; + +describe('Initialized CodeCommit Repository', () => { + const props: CdkExtensions.RepositoryProps = { + repositoryName: 'AWS-accelerator', + repositoryBranchName: 'main', + s3BucketName: 'Testbucket', + s3key: 'testkey', + }; + + test('Initialization Properties Test', () => { + const stack = new cdk.Stack(); + + new CdkExtensions.Repository(stack, 'SnapshotTest', props); + + expectCDK(stack).to( + haveResource('AWS::CodeCommit::Repository', { + RepositoryName: props.repositoryName, + Code: { + BranchName: props.repositoryBranchName, + S3: { + Bucket: props.s3BucketName, + Key: props.s3key, + }, + }, + }), + ); + }); + + /** + * Snapshot Test - Initialzed Repository + */ + test('Snapshot Test', () => { + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'TestStack'); + + new CdkExtensions.Repository(stack, 'SnapshotTest', { + repositoryBranchName: props.repositoryBranchName, + repositoryName: props.repositoryName, + s3BucketName: props.s3BucketName, + s3key: props.s3key, + }); + + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); + }); +}); diff --git a/source/packages/@aws-cdk-extensions/cdk-extensions/test/test-config.ts b/source/packages/@aws-cdk-extensions/cdk-extensions/test/test-config.ts new file mode 100644 index 000000000..cf49560ed --- /dev/null +++ b/source/packages/@aws-cdk-extensions/cdk-extensions/test/test-config.ts @@ -0,0 +1,29 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { Stack } from 'aws-cdk-lib'; +import * as CdkExtensions from '../index'; + +/** + * Stack Initialization + */ +export const stack = new Stack(); +/** + * Accelerator Pipeline Secure Bucket Properties + */ +export const repositoryProps: CdkExtensions.RepositoryProps = { + repositoryName: 'AWS-accelerator', + repositoryBranchName: 'main', + s3BucketName: 'Testbucket', + s3key: 'testkey', +}; diff --git a/source/packages/@aws-cdk-extensions/cdk-extensions/tsconfig.json b/source/packages/@aws-cdk-extensions/cdk-extensions/tsconfig.json new file mode 100644 index 000000000..fc62a1e09 --- /dev/null +++ b/source/packages/@aws-cdk-extensions/cdk-extensions/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["lib/**/*", "index.ts"], + "exclude": ["test/**/*"] +} diff --git a/source/packages/@aws-cdk-extensions/cdk-plugin-assume-role/.gitignore b/source/packages/@aws-cdk-extensions/cdk-plugin-assume-role/.gitignore new file mode 100644 index 000000000..53c37a166 --- /dev/null +++ b/source/packages/@aws-cdk-extensions/cdk-plugin-assume-role/.gitignore @@ -0,0 +1 @@ +dist \ No newline at end of file diff --git a/source/packages/@aws-cdk-extensions/cdk-plugin-assume-role/index.ts b/source/packages/@aws-cdk-extensions/cdk-plugin-assume-role/index.ts new file mode 100644 index 000000000..e68fc33b1 --- /dev/null +++ b/source/packages/@aws-cdk-extensions/cdk-plugin-assume-role/index.ts @@ -0,0 +1,15 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +export * from './lib/assume-role-plugin'; +export * from './lib/assume-role-provider-source'; diff --git a/source/packages/@aws-cdk-extensions/cdk-plugin-assume-role/lib/assume-role-plugin.ts b/source/packages/@aws-cdk-extensions/cdk-plugin-assume-role/lib/assume-role-plugin.ts new file mode 100644 index 000000000..4d7292a95 --- /dev/null +++ b/source/packages/@aws-cdk-extensions/cdk-plugin-assume-role/lib/assume-role-plugin.ts @@ -0,0 +1,51 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { Plugin, PluginHost } from 'aws-cdk/lib/plugin'; +import * as AWS from 'aws-sdk'; +import { AssumeRoleProviderSource } from './assume-role-provider-source'; + +export class AssumeProfilePlugin implements Plugin { + readonly version = '1'; + + constructor( + private readonly props: { + assumeRoleName?: string; + assumeRoleDuration?: number; + credentials?: AWS.STS.Credentials; + partition?: string; + } = {}, + ) {} + + init(host: PluginHost): void { + const source = new AssumeRoleProviderSource({ + name: 'cdk-assume-role-plugin', + assumeRoleName: this.props.assumeRoleName ?? AssumeProfilePlugin.getDefaultAssumeRoleName(), + assumeRoleDuration: this.props.assumeRoleDuration ?? AssumeProfilePlugin.getDefaultAssumeRoleDuration(), + credentials: this.props.credentials, + partition: this.props.partition, + }); + host.registerCredentialProviderSource(source); + } + + static getDefaultAssumeRoleName(): string { + return process.env['CDK_PLUGIN_ASSUME_ROLE_NAME']!; + } + + static getDefaultAssumeRoleDuration(): number { + if (process.env['CDK_PLUGIN_ASSUME_ROLE_DURATION']) { + return +process.env['CDK_PLUGIN_ASSUME_ROLE_DURATION']; + } + return 3600; + } +} diff --git a/source/packages/@aws-cdk-extensions/cdk-plugin-assume-role/lib/assume-role-provider-source.ts b/source/packages/@aws-cdk-extensions/cdk-plugin-assume-role/lib/assume-role-provider-source.ts new file mode 100644 index 000000000..ef6e7972c --- /dev/null +++ b/source/packages/@aws-cdk-extensions/cdk-plugin-assume-role/lib/assume-role-provider-source.ts @@ -0,0 +1,90 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { CredentialProviderSource } from 'aws-cdk/lib/api/aws-auth/credentials'; +import * as AWS from 'aws-sdk'; +import { green } from 'colors/safe'; +import { throttlingBackOff } from './backoff'; + +export interface AssumeRoleProviderSourceProps { + name: string; + assumeRoleName: string; + assumeRoleDuration: number; + credentials?: AWS.STS.Credentials; + partition?: string; +} + +export class AssumeRoleProviderSource implements CredentialProviderSource { + readonly name = this.props.name; + private readonly cache: { [accountId: string]: AWS.Credentials } = {}; + + constructor(private readonly props: AssumeRoleProviderSourceProps) {} + + async isAvailable(): Promise { + return true; + } + + async canProvideCredentials(): Promise { + return true; + } + + async getProvider(accountId: string): Promise { + if (this.cache[accountId]) { + return this.cache[accountId]; + } + + let assumeRole; + try { + // Try to assume the role with the given duration + assumeRole = await this.assumeRole(accountId, this.props.assumeRoleDuration); + } catch (e) { + console.warn(`Cannot assume role for ${this.props.assumeRoleDuration} seconds: ${e}`); + + // If that fails, than try to assume the role for one hour + assumeRole = await this.assumeRole(accountId, 3600); + } + + const credentials = assumeRole.Credentials!; + return (this.cache[accountId] = new AWS.Credentials({ + accessKeyId: credentials.AccessKeyId, + secretAccessKey: credentials.SecretAccessKey, + sessionToken: credentials.SessionToken, + })); + } + + protected async assumeRole(accountId: string, duration: number): Promise { + const roleArn = `arn:${this.props.partition ?? 'aws'}:iam::${accountId}:role/${this.props.assumeRoleName}`; + console.log(`Assuming role ${green(roleArn)} for ${duration} seconds`); + + let sts: AWS.STS; + if (this.props.credentials) { + sts = new AWS.STS({ + accessKeyId: this.props.credentials.AccessKeyId, + secretAccessKey: this.props.credentials.SecretAccessKey, + sessionToken: this.props.credentials.SessionToken, + }); + } else { + sts = new AWS.STS(); + } + + return throttlingBackOff(() => + sts + .assumeRole({ + RoleArn: roleArn, + RoleSessionName: this.name, + DurationSeconds: duration, + }) + .promise(), + ); + } +} diff --git a/source/packages/@aws-cdk-extensions/cdk-plugin-assume-role/lib/backoff.ts b/source/packages/@aws-cdk-extensions/cdk-plugin-assume-role/lib/backoff.ts new file mode 100644 index 000000000..ceb0b6c40 --- /dev/null +++ b/source/packages/@aws-cdk-extensions/cdk-plugin-assume-role/lib/backoff.ts @@ -0,0 +1,44 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { backOff, IBackOffOptions } from 'exponential-backoff'; + +/** + * Auxiliary function to retry AWS SDK calls when a throttling error occurs. + */ +export function throttlingBackOff( + request: () => Promise, + options?: Partial>, +): Promise { + return backOff(request, { + startingDelay: 500, + jitter: 'full', + retry: isThrottlingError, + ...options, + }); +} + +export const isThrottlingError = ( + e: any, // eslint-disable-line +): boolean => + e.retryable === true || + // SDKv2 Error Structure + e.code === 'TooManyRequestsException' || + e.code === 'Throttling' || + e.code === 'ThrottlingException' || + e.code === 'InternalException' || + // SDKv3 Error Structure + e.name === 'TooManyRequestsException' || + e.name === 'Throttling' || + e.name === 'ThrottlingException' || + e.name === 'InternalException'; diff --git a/source/packages/@aws-cdk-extensions/cdk-plugin-assume-role/package.json b/source/packages/@aws-cdk-extensions/cdk-plugin-assume-role/package.json new file mode 100644 index 000000000..72f66a7f3 --- /dev/null +++ b/source/packages/@aws-cdk-extensions/cdk-plugin-assume-role/package.json @@ -0,0 +1,45 @@ +{ + "name": "@aws-cdk-extensions/cdk-plugin-assume-role", + "version": "0.0.0", + "private": true, + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "cleanup": "tsc --build ./ --clean && rm -rf node_modules && rm -rf yarn.lock && rm -rf dist", + "cleanup:tsc": "tsc --build ./ --clean", + "build": "tsc", + "watch": "tsc -w", + "test": "", + "lint": "eslint --fix --max-warnings 0 -c ../../../.eslintrc.json 'lib/**/*.{ts,tsx}'", + "precommit": "eslint --max-warnings 0 -c ../../../.eslintrc.json 'lib/**/*.{ts,tsx}'" + }, + "dependencies": { + "@typescript-eslint/eslint-plugin": "5.6.0", + "@typescript-eslint/parser": "5.6.0", + "aws-cdk-lib": "2.16.0", + "aws-cdk": "2.16.0", + "aws-sdk": "2.1046.0", + "colors": "1.4.0", + "exponential-backoff": "3.1.0" + }, + "devDependencies": { + "@types/node": "16.11.12", + "eslint": "8.4.1", + "eslint-config-prettier": "8.3.0", + "eslint-config-standard": "16.0.3", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.0.0", + "prettier": "2.5.1", + "typescript": "4.5.2" + }, + "nohoist": [ + "**/exponential-backoff", + "**/exponential-backoff/**" + ] +} diff --git a/source/packages/@aws-cdk-extensions/cdk-plugin-assume-role/tsconfig.json b/source/packages/@aws-cdk-extensions/cdk-plugin-assume-role/tsconfig.json new file mode 100644 index 000000000..fc62a1e09 --- /dev/null +++ b/source/packages/@aws-cdk-extensions/cdk-plugin-assume-role/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["lib/**/*", "index.ts"], + "exclude": ["test/**/*"] +} diff --git a/source/run-all-tests.sh b/source/run-all-tests.sh new file mode 100755 index 000000000..311b978c8 --- /dev/null +++ b/source/run-all-tests.sh @@ -0,0 +1,157 @@ +#!/bin/bash +# +# This script runs all tests for the root CDK project, as well as any microservices, Lambda functions, or dependency +# source code packages. These include unit tests, integration tests, and snapshot tests. +# +# This script is called by the ../initialize-repo.sh file and the buildspec.yml file. It is important that this script +# be tested and validated to ensure that all available test fixtures are run. +# +# The if/then blocks are for error handling. They will cause the script to stop executing if an error is thrown from the +# node process running the test case(s). Removing them or not using them for additional calls with result in the +# script continuing to execute despite an error being thrown. + +[ "$DEBUG" == 'true' ] && set -x +set -e + +setup_python_env() { + if [ -d "./.venv-test" ]; then + echo "Reusing already setup python venv in ./.venv-test. Delete ./.venv-test if you want a fresh one created." + return + fi + + echo "Setting up python venv" + python3 -m venv .venv-test + echo "Initiating virtual environment" + source .venv-test/bin/activate + + echo "Installing python packages" + # install test dependencies in the python virtual environment + pip3 install -r requirements-test.txt + pip3 install -r requirements.txt --target . + + echo "deactivate virtual environment" + deactivate +} + +run_python_test() { + local component_path=$1 + local component_name=$2 + + echo "------------------------------------------------------------------------------" + echo "[Test] Run python unit test with coverage for $component_path $component_name" + echo "------------------------------------------------------------------------------" + cd $component_path + + if [ "${CLEAN:-true}" = "true" ]; then + rm -fr .venv-test + fi + + setup_python_env + + echo "Initiating virtual environment" + source .venv-test/bin/activate + + # setup coverage report path + mkdir -p $source_dir/test/coverage-reports + coverage_report_path=$source_dir/test/coverage-reports/$component_name.coverage.xml + echo "coverage report path set to $coverage_report_path" + + # Use -vv for debugging + python3 -m pytest --cov --cov-report=term-missing --cov-report "xml:$coverage_report_path" + + # The pytest --cov with its parameters and .coveragerc generates a xml cov-report with `coverage/sources` list + # with absolute path for the source directories. To avoid dependencies of tools (such as SonarQube) on different + # absolute paths for source directories, this substitution is used to convert each absolute source directory + # path to the corresponding project relative path. The $source_dir holds the absolute path for source directory. + sed -i -e "s,$source_dir,source,g" $coverage_report_path + + echo "deactivate virtual environment" + deactivate + + if [ "${CLEAN:-true}" = "true" ]; then + rm -fr .venv-test + rm .coverage + rm -fr .pytest_cache + rm -fr __pycache__ test/__pycache__ + fi +} + +prepare_jest_coverage_report() { + local component_name=$1 + + if [ ! -d "coverage" ]; then + echo "ValidationError: Missing required directory coverage after running unit tests" + exit 129 + fi + + # prepare coverage reports + rm -fr coverage/lcov-report + mkdir -p $coverage_reports_top_path/jest + coverage_report_path=$coverage_reports_top_path/jest/$component_name + rm -fr $coverage_report_path + mv coverage $coverage_report_path +} + +run_javascript_test() { + local component_path=$1 + local component_name=$2 + + echo "------------------------------------------------------------------------------" + echo "[Test] Run javascript unit test with coverage for $component_path $component_name" + echo "------------------------------------------------------------------------------" + echo "cd $component_path" + cd $component_path + + # install and build for unit testing + npm install + + # run unit tests + npm run test + + # prepare coverage reports + prepare_jest_coverage_report $component_name +} + +run_cdk_project_test() { + local component_path=$1 + local component_name=solutions-constructs + + echo "------------------------------------------------------------------------------" + echo "[Test] $component_name" + echo "------------------------------------------------------------------------------" + cd $component_path + + # install and build for unit testing + yarn lerna bootstrap + yarn build + + ## Option to suppress the Override Warning messages while synthesizing using CDK + # export overrideWarningsEnabled=false + + # run unit tests + yarn run test + + # prepare coverage reports + prepare_jest_coverage_report $component_name +} + +# Run unit tests +echo "Running unit tests" + +# Get reference for source folder +source_dir="$(cd $PWD/../source; pwd -P)" +coverage_reports_top_path=$source_dir/test/coverage-reports + +# Install lerna +echo "Install lerna" +yarn add lerna@^4.0.0 -W + +# TODO: Update to handle workspaces +# Test the CDK project +# run_cdk_project_test $source_dir + +# Test the attached Lambda function +# run_javascript_test $source_dir/lambda/example-function-js example-function-js + +# Return to the source/ level +cd $source_dir \ No newline at end of file diff --git a/source/tsconfig.json b/source/tsconfig.json new file mode 100644 index 000000000..70672b24f --- /dev/null +++ b/source/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "alwaysStrict": true, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noPropertyAccessFromIndexSignature": true, + "noUnusedParameters": true, + "noUnusedLocals": true, + "resolveJsonModule": true, + "strict": true, + "module": "commonjs", + "declaration": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "preserveSymlinks": true, + "sourceMap": true, + "lib": ["ES2019", "ES2020.Promise"], + "target": "ES2019", + "composite": false + } +} diff --git a/source/yarn.lock b/source/yarn.lock new file mode 100644 index 000000000..06fe05a09 --- /dev/null +++ b/source/yarn.lock @@ -0,0 +1,10419 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@ampproject/remapping@^2.1.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.1.2.tgz#4edca94973ded9630d20101cd8559cedb8d8bd34" + integrity sha512-hoyByceqwKirw7w3Z7gnIIZC3Wx3J484Y3L/cMpXFbr7d9ZQj2mODrirNzcJa+SM3UlpWXYvKV4RlRpFXlWgXg== + dependencies: + "@jridgewell/trace-mapping" "^0.3.0" + +"@aws-cdk/assert@2.16.0": + version "2.16.0" + resolved "https://registry.yarnpkg.com/@aws-cdk/assert/-/assert-2.16.0.tgz#90286a55420abfe6144d1d059c5b020743c7f61e" + integrity sha512-CHq1/8mW/g4I2aXuav5b/ilegQfyu/bJ5DNiL/nrwH6KQQj/WakDGWK0IdGWDJT+e8BD3h1kr+kd3V91eqjlag== + dependencies: + "@aws-cdk/cloudformation-diff" "2.16.0" + +"@aws-cdk/cfnspec@2.16.0": + version "2.16.0" + resolved "https://registry.yarnpkg.com/@aws-cdk/cfnspec/-/cfnspec-2.16.0.tgz#5623aff2b9cdd54a76c2c9997a2fc09662148855" + integrity sha512-mnIDGvi0iuqP70wlasbW7gNdGf+/B2jL4azMfJjNZ9hRnT7Sgh9F7KpW88KJd0d8GfWgGR+VS6ufsaZGex+tyw== + dependencies: + fs-extra "^9.1.0" + md5 "^2.3.0" + +"@aws-cdk/cloud-assembly-schema@2.16.0": + version "2.16.0" + resolved "https://registry.yarnpkg.com/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-2.16.0.tgz#a8ec611b3c986d5a4f356978e49f012bfb4cbd4f" + integrity sha512-Ssh5hpyDaXcor5hUkSR1hJa2z7TUa8yGnHTOZR59GFljyxQA3qY0Yf03623PMID92IpHdMp5rfD2F2Moj8v2yQ== + dependencies: + jsonschema "^1.4.0" + semver "^7.3.5" + +"@aws-cdk/cloudformation-diff@2.16.0": + version "2.16.0" + resolved "https://registry.yarnpkg.com/@aws-cdk/cloudformation-diff/-/cloudformation-diff-2.16.0.tgz#5c6094d880eda5f999f7d4f00119870b58199a33" + integrity sha512-EXl5PPf8CIXcDPZMjkjpwYw5Q7CfCG2FPQU8G6etPLVFWQVzGGtZ0L03xPunTzE2VT8g4V6bk9+1HAq1lP9wMA== + dependencies: + "@aws-cdk/cfnspec" "2.16.0" + "@types/node" "^10.17.60" + chalk "^4" + diff "^5.0.0" + fast-deep-equal "^3.1.3" + string-width "^4.2.3" + table "^6.8.0" + +"@aws-cdk/cx-api@2.16.0": + version "2.16.0" + resolved "https://registry.yarnpkg.com/@aws-cdk/cx-api/-/cx-api-2.16.0.tgz#cd7bdc9cbbc8f66223b86745ee321866922e9760" + integrity sha512-wG+ulGL6GYeTLvkTYwgqCg/i/dKihPAoR67dUTi1dwGE8E0ByXitqS+fV7ggEZ6pKCqiQeL92BkRivkYk7kRrw== + dependencies: + "@aws-cdk/cloud-assembly-schema" "2.16.0" + semver "^7.3.5" + +"@aws-cdk/region-info@2.16.0": + version "2.16.0" + resolved "https://registry.yarnpkg.com/@aws-cdk/region-info/-/region-info-2.16.0.tgz#069b7da459b5422e2f48c78b6aede8871b47c4b0" + integrity sha512-g3pt13CSwdAGDSfX/uquRxv0S7TVtihym6h2phW+w/xtCT0fVpYG2xu3LX+lydHEoeEFjW771uXhVTY2PadJyQ== + +"@aws-crypto/crc32@2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/crc32/-/crc32-2.0.0.tgz#4ad432a3c03ec3087c5540ff6e41e6565d2dc153" + integrity sha512-TvE1r2CUueyXOuHdEigYjIZVesInd9KN+K/TFFNfkkxRThiNxO6i4ZqqAVMoEjAamZZ1AA8WXJkjCz7YShHPQA== + dependencies: + "@aws-crypto/util" "^2.0.0" + "@aws-sdk/types" "^3.1.0" + tslib "^1.11.1" + +"@aws-crypto/crc32c@2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/crc32c/-/crc32c-2.0.0.tgz#4235336ef78f169f6a05248906703b9b78da676e" + integrity sha512-vF0eMdMHx3O3MoOXUfBZry8Y4ZDtcuskjjKgJz8YfIDjLStxTZrYXk+kZqtl6A0uCmmiN/Eb/JbC/CndTV1MHg== + dependencies: + "@aws-crypto/util" "^2.0.0" + "@aws-sdk/types" "^3.1.0" + tslib "^1.11.1" + +"@aws-crypto/ie11-detection@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/ie11-detection/-/ie11-detection-2.0.0.tgz#bb6c2facf8f03457e949dcf0921477397ffa4c6e" + integrity sha512-pkVXf/dq6PITJ0jzYZ69VhL8VFOFoPZLZqtU/12SGnzYuJOOGNfF41q9GxdI1yqC8R13Rq3jOLKDFpUJFT5eTA== + dependencies: + tslib "^1.11.1" + +"@aws-crypto/sha1-browser@2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/sha1-browser/-/sha1-browser-2.0.0.tgz#71e735df20ea1d38f59259c4b1a2e00ca74a0eea" + integrity sha512-3fIVRjPFY8EG5HWXR+ZJZMdWNRpwbxGzJ9IH9q93FpbgCH8u8GHRi46mZXp3cYD7gealmyqpm3ThZwLKJjWJhA== + dependencies: + "@aws-crypto/ie11-detection" "^2.0.0" + "@aws-crypto/supports-web-crypto" "^2.0.0" + "@aws-sdk/types" "^3.1.0" + "@aws-sdk/util-locate-window" "^3.0.0" + "@aws-sdk/util-utf8-browser" "^3.0.0" + tslib "^1.11.1" + +"@aws-crypto/sha256-browser@2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-browser/-/sha256-browser-2.0.0.tgz#741c9024df55ec59b51e5b1f5d806a4852699fb5" + integrity sha512-rYXOQ8BFOaqMEHJrLHul/25ckWH6GTJtdLSajhlqGMx0PmSueAuvboCuZCTqEKlxR8CQOwRarxYMZZSYlhRA1A== + dependencies: + "@aws-crypto/ie11-detection" "^2.0.0" + "@aws-crypto/sha256-js" "^2.0.0" + "@aws-crypto/supports-web-crypto" "^2.0.0" + "@aws-crypto/util" "^2.0.0" + "@aws-sdk/types" "^3.1.0" + "@aws-sdk/util-locate-window" "^3.0.0" + "@aws-sdk/util-utf8-browser" "^3.0.0" + tslib "^1.11.1" + +"@aws-crypto/sha256-js@2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-js/-/sha256-js-2.0.0.tgz#f1f936039bdebd0b9e2dd834d65afdc2aac4efcb" + integrity sha512-VZY+mCY4Nmrs5WGfitmNqXzaE873fcIZDu54cbaDaaamsaTOP1DBImV9F4pICc3EHjQXujyE8jig+PFCaew9ig== + dependencies: + "@aws-crypto/util" "^2.0.0" + "@aws-sdk/types" "^3.1.0" + tslib "^1.11.1" + +"@aws-crypto/sha256-js@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-js/-/sha256-js-2.0.1.tgz#79e1e6cf61f652ef2089c08d471c722ecf1626a9" + integrity sha512-mbHTBSPBvg6o/mN/c18Z/zifM05eJrapj5ggoOIeHIWckvkv5VgGi7r/wYpt+QAO2ySKXLNvH2d8L7bne4xrMQ== + dependencies: + "@aws-crypto/util" "^2.0.1" + "@aws-sdk/types" "^3.1.0" + tslib "^1.11.1" + +"@aws-crypto/supports-web-crypto@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/supports-web-crypto/-/supports-web-crypto-2.0.0.tgz#fd6cde30b88f77d5a4f57b2c37c560d918014f9e" + integrity sha512-Ge7WQ3E0OC7FHYprsZV3h0QIcpdyJLvIeg+uTuHqRYm8D6qCFJoiC+edSzSyFiHtZf+NOQDJ1q46qxjtzIY2nA== + dependencies: + tslib "^1.11.1" + +"@aws-crypto/util@^2.0.0", "@aws-crypto/util@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@aws-crypto/util/-/util-2.0.1.tgz#976cf619cf85084ca85ec5eb947a6ac6b8b5c98c" + integrity sha512-JJmFFwvbm08lULw4Nm5QOLg8+lAQeC8aCXK5xrtxntYzYXCGfHwUJ4Is3770Q7HmICsXthGQ+ZsDL7C2uH3yBQ== + dependencies: + "@aws-sdk/types" "^3.1.0" + "@aws-sdk/util-utf8-browser" "^3.0.0" + tslib "^1.11.1" + +"@aws-sdk/abort-controller@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/abort-controller/-/abort-controller-3.78.0.tgz#f2b0f8d63954afe51136254f389a18dd24a8f6f3" + integrity sha512-iz1YLwM2feJUj/y97yO4XmDeTxs+yZ1XJwQgoawKuc8IDBKUutnJNCHL5jL04WUKU7Nrlq+Hr2fCTScFh2z9zg== + dependencies: + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/chunked-blob-reader-native@3.58.0": + version "3.58.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/chunked-blob-reader-native/-/chunked-blob-reader-native-3.58.0.tgz#1db413c5c80b32e24f1b62b22e15e9ad74d75cda" + integrity sha512-+D3xnPD5985iphgAqgUerBDs371a2WzzoEVi7eHJUMMsP/gEnSTdSH0HNxsqhYv6CW4EdKtvDAQdAwA1VtCf2A== + dependencies: + "@aws-sdk/util-base64-browser" "3.58.0" + tslib "^2.3.1" + +"@aws-sdk/chunked-blob-reader@3.55.0": + version "3.55.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/chunked-blob-reader/-/chunked-blob-reader-3.55.0.tgz#db240c78e7c4c826e707f0ca32a4d221c41cf6a0" + integrity sha512-o/xjMCq81opAjSBjt7YdHJwIJcGVG5XIV9+C2KXcY5QwVimkOKPybWTv0mXPvSwSilSx+EhpLNhkcJuXdzhw4w== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/client-backup@3.92.0": + version "3.92.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-backup/-/client-backup-3.92.0.tgz#6180e467ae63c151d012f32d0ed2a7e8db3b2b94" + integrity sha512-Sqr3V4YjzUEody4VXg8VuaRY3Vz2wekNHP7DIp1Gfp0J9JhklGoplpHOEmepkcoCraiQUTRex+S4iFLdcMRrIw== + dependencies: + "@aws-crypto/sha256-browser" "2.0.0" + "@aws-crypto/sha256-js" "2.0.0" + "@aws-sdk/client-sts" "3.92.0" + "@aws-sdk/config-resolver" "3.80.0" + "@aws-sdk/credential-provider-node" "3.87.0" + "@aws-sdk/fetch-http-handler" "3.78.0" + "@aws-sdk/hash-node" "3.78.0" + "@aws-sdk/invalid-dependency" "3.78.0" + "@aws-sdk/middleware-content-length" "3.78.0" + "@aws-sdk/middleware-host-header" "3.78.0" + "@aws-sdk/middleware-logger" "3.78.0" + "@aws-sdk/middleware-retry" "3.80.0" + "@aws-sdk/middleware-serde" "3.78.0" + "@aws-sdk/middleware-signing" "3.78.0" + "@aws-sdk/middleware-stack" "3.78.0" + "@aws-sdk/middleware-user-agent" "3.78.0" + "@aws-sdk/node-config-provider" "3.80.0" + "@aws-sdk/node-http-handler" "3.82.0" + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/smithy-client" "3.85.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/url-parser" "3.78.0" + "@aws-sdk/util-base64-browser" "3.58.0" + "@aws-sdk/util-base64-node" "3.55.0" + "@aws-sdk/util-body-length-browser" "3.55.0" + "@aws-sdk/util-body-length-node" "3.55.0" + "@aws-sdk/util-defaults-mode-browser" "3.85.0" + "@aws-sdk/util-defaults-mode-node" "3.85.0" + "@aws-sdk/util-user-agent-browser" "3.78.0" + "@aws-sdk/util-user-agent-node" "3.80.0" + "@aws-sdk/util-utf8-browser" "3.55.0" + "@aws-sdk/util-utf8-node" "3.55.0" + tslib "^2.3.1" + uuid "^8.3.2" + +"@aws-sdk/client-cloudformation@3.87.0": + version "3.87.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-cloudformation/-/client-cloudformation-3.87.0.tgz#bd85edfa7cb97f7af0be3a9d6052869325117d44" + integrity sha512-6XwvHdiBIpVkHmu3IisWOFfNeGKLX9aknoBLDvYCm0RQbTlbM3mgkOI5NJvZ6oyQk8YAm4l/pnYJ5MEsSxMRxg== + dependencies: + "@aws-crypto/sha256-browser" "2.0.0" + "@aws-crypto/sha256-js" "2.0.0" + "@aws-sdk/client-sts" "3.87.0" + "@aws-sdk/config-resolver" "3.80.0" + "@aws-sdk/credential-provider-node" "3.87.0" + "@aws-sdk/fetch-http-handler" "3.78.0" + "@aws-sdk/hash-node" "3.78.0" + "@aws-sdk/invalid-dependency" "3.78.0" + "@aws-sdk/middleware-content-length" "3.78.0" + "@aws-sdk/middleware-host-header" "3.78.0" + "@aws-sdk/middleware-logger" "3.78.0" + "@aws-sdk/middleware-retry" "3.80.0" + "@aws-sdk/middleware-serde" "3.78.0" + "@aws-sdk/middleware-signing" "3.78.0" + "@aws-sdk/middleware-stack" "3.78.0" + "@aws-sdk/middleware-user-agent" "3.78.0" + "@aws-sdk/node-config-provider" "3.80.0" + "@aws-sdk/node-http-handler" "3.82.0" + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/smithy-client" "3.85.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/url-parser" "3.78.0" + "@aws-sdk/util-base64-browser" "3.58.0" + "@aws-sdk/util-base64-node" "3.55.0" + "@aws-sdk/util-body-length-browser" "3.55.0" + "@aws-sdk/util-body-length-node" "3.55.0" + "@aws-sdk/util-defaults-mode-browser" "3.85.0" + "@aws-sdk/util-defaults-mode-node" "3.85.0" + "@aws-sdk/util-user-agent-browser" "3.78.0" + "@aws-sdk/util-user-agent-node" "3.80.0" + "@aws-sdk/util-utf8-browser" "3.55.0" + "@aws-sdk/util-utf8-node" "3.55.0" + "@aws-sdk/util-waiter" "3.78.0" + entities "2.2.0" + fast-xml-parser "3.19.0" + tslib "^2.3.1" + uuid "^8.3.2" + +"@aws-sdk/client-cloudformation@3.92.0": + version "3.92.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-cloudformation/-/client-cloudformation-3.92.0.tgz#5c1f48dec460a05c5464e3c4ba98b053ff714ac6" + integrity sha512-AwmL3+tfUlE0+wgtneougfqWXmzIZldtzRzopW2PtPKeMit98f7a9ri/EjXgrE+NFgFE/uGhcpNwLNYGQ449JQ== + dependencies: + "@aws-crypto/sha256-browser" "2.0.0" + "@aws-crypto/sha256-js" "2.0.0" + "@aws-sdk/client-sts" "3.92.0" + "@aws-sdk/config-resolver" "3.80.0" + "@aws-sdk/credential-provider-node" "3.87.0" + "@aws-sdk/fetch-http-handler" "3.78.0" + "@aws-sdk/hash-node" "3.78.0" + "@aws-sdk/invalid-dependency" "3.78.0" + "@aws-sdk/middleware-content-length" "3.78.0" + "@aws-sdk/middleware-host-header" "3.78.0" + "@aws-sdk/middleware-logger" "3.78.0" + "@aws-sdk/middleware-retry" "3.80.0" + "@aws-sdk/middleware-serde" "3.78.0" + "@aws-sdk/middleware-signing" "3.78.0" + "@aws-sdk/middleware-stack" "3.78.0" + "@aws-sdk/middleware-user-agent" "3.78.0" + "@aws-sdk/node-config-provider" "3.80.0" + "@aws-sdk/node-http-handler" "3.82.0" + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/smithy-client" "3.85.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/url-parser" "3.78.0" + "@aws-sdk/util-base64-browser" "3.58.0" + "@aws-sdk/util-base64-node" "3.55.0" + "@aws-sdk/util-body-length-browser" "3.55.0" + "@aws-sdk/util-body-length-node" "3.55.0" + "@aws-sdk/util-defaults-mode-browser" "3.85.0" + "@aws-sdk/util-defaults-mode-node" "3.85.0" + "@aws-sdk/util-user-agent-browser" "3.78.0" + "@aws-sdk/util-user-agent-node" "3.80.0" + "@aws-sdk/util-utf8-browser" "3.55.0" + "@aws-sdk/util-utf8-node" "3.55.0" + "@aws-sdk/util-waiter" "3.78.0" + entities "2.2.0" + fast-xml-parser "3.19.0" + tslib "^2.3.1" + uuid "^8.3.2" + +"@aws-sdk/client-cloudwatch-logs@3.92.0": + version "3.92.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-cloudwatch-logs/-/client-cloudwatch-logs-3.92.0.tgz#23925595bf0b104e9eb1b4a1278e1e3088ed8b3a" + integrity sha512-6Wqdjy1aoQStZjdGPOj877bBoBUGutnVKXIi7axCiADC4yMP58j+P8tE5kXGUgI164zkz9tSoas4tlazS8QUPA== + dependencies: + "@aws-crypto/sha256-browser" "2.0.0" + "@aws-crypto/sha256-js" "2.0.0" + "@aws-sdk/client-sts" "3.92.0" + "@aws-sdk/config-resolver" "3.80.0" + "@aws-sdk/credential-provider-node" "3.87.0" + "@aws-sdk/fetch-http-handler" "3.78.0" + "@aws-sdk/hash-node" "3.78.0" + "@aws-sdk/invalid-dependency" "3.78.0" + "@aws-sdk/middleware-content-length" "3.78.0" + "@aws-sdk/middleware-host-header" "3.78.0" + "@aws-sdk/middleware-logger" "3.78.0" + "@aws-sdk/middleware-retry" "3.80.0" + "@aws-sdk/middleware-serde" "3.78.0" + "@aws-sdk/middleware-signing" "3.78.0" + "@aws-sdk/middleware-stack" "3.78.0" + "@aws-sdk/middleware-user-agent" "3.78.0" + "@aws-sdk/node-config-provider" "3.80.0" + "@aws-sdk/node-http-handler" "3.82.0" + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/smithy-client" "3.85.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/url-parser" "3.78.0" + "@aws-sdk/util-base64-browser" "3.58.0" + "@aws-sdk/util-base64-node" "3.55.0" + "@aws-sdk/util-body-length-browser" "3.55.0" + "@aws-sdk/util-body-length-node" "3.55.0" + "@aws-sdk/util-defaults-mode-browser" "3.85.0" + "@aws-sdk/util-defaults-mode-node" "3.85.0" + "@aws-sdk/util-user-agent-browser" "3.78.0" + "@aws-sdk/util-user-agent-node" "3.80.0" + "@aws-sdk/util-utf8-browser" "3.55.0" + "@aws-sdk/util-utf8-node" "3.55.0" + tslib "^2.3.1" + +"@aws-sdk/client-codebuild@3.92.0": + version "3.92.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-codebuild/-/client-codebuild-3.92.0.tgz#83354967a867989e147cb10f2346083e2e179950" + integrity sha512-qsOE+ATYnKc8K+xD4I6Kmt+mPHCqrQcCpfBNXIDvaiKsaCNTsiAkzrSxpfy9MldiuOA7849Wdw34t/2juTPa3Q== + dependencies: + "@aws-crypto/sha256-browser" "2.0.0" + "@aws-crypto/sha256-js" "2.0.0" + "@aws-sdk/client-sts" "3.92.0" + "@aws-sdk/config-resolver" "3.80.0" + "@aws-sdk/credential-provider-node" "3.87.0" + "@aws-sdk/fetch-http-handler" "3.78.0" + "@aws-sdk/hash-node" "3.78.0" + "@aws-sdk/invalid-dependency" "3.78.0" + "@aws-sdk/middleware-content-length" "3.78.0" + "@aws-sdk/middleware-host-header" "3.78.0" + "@aws-sdk/middleware-logger" "3.78.0" + "@aws-sdk/middleware-retry" "3.80.0" + "@aws-sdk/middleware-serde" "3.78.0" + "@aws-sdk/middleware-signing" "3.78.0" + "@aws-sdk/middleware-stack" "3.78.0" + "@aws-sdk/middleware-user-agent" "3.78.0" + "@aws-sdk/node-config-provider" "3.80.0" + "@aws-sdk/node-http-handler" "3.82.0" + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/smithy-client" "3.85.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/url-parser" "3.78.0" + "@aws-sdk/util-base64-browser" "3.58.0" + "@aws-sdk/util-base64-node" "3.55.0" + "@aws-sdk/util-body-length-browser" "3.55.0" + "@aws-sdk/util-body-length-node" "3.55.0" + "@aws-sdk/util-defaults-mode-browser" "3.85.0" + "@aws-sdk/util-defaults-mode-node" "3.85.0" + "@aws-sdk/util-user-agent-browser" "3.78.0" + "@aws-sdk/util-user-agent-node" "3.80.0" + "@aws-sdk/util-utf8-browser" "3.55.0" + "@aws-sdk/util-utf8-node" "3.55.0" + tslib "^2.3.1" + +"@aws-sdk/client-codecommit@3.92.0": + version "3.92.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-codecommit/-/client-codecommit-3.92.0.tgz#dfaf50a1e036d41b2a1fd4da7e7fb63cfb655b33" + integrity sha512-AD/YZgo0Aq3OH0IWanRaTTAOsF4wzSt5Drd2WUmi1S+l9k4TBW94NtAR2dWbksWK04vm6GHy97R51PsZS7+qYw== + dependencies: + "@aws-crypto/sha256-browser" "2.0.0" + "@aws-crypto/sha256-js" "2.0.0" + "@aws-sdk/client-sts" "3.92.0" + "@aws-sdk/config-resolver" "3.80.0" + "@aws-sdk/credential-provider-node" "3.87.0" + "@aws-sdk/fetch-http-handler" "3.78.0" + "@aws-sdk/hash-node" "3.78.0" + "@aws-sdk/invalid-dependency" "3.78.0" + "@aws-sdk/middleware-content-length" "3.78.0" + "@aws-sdk/middleware-host-header" "3.78.0" + "@aws-sdk/middleware-logger" "3.78.0" + "@aws-sdk/middleware-retry" "3.80.0" + "@aws-sdk/middleware-serde" "3.78.0" + "@aws-sdk/middleware-signing" "3.78.0" + "@aws-sdk/middleware-stack" "3.78.0" + "@aws-sdk/middleware-user-agent" "3.78.0" + "@aws-sdk/node-config-provider" "3.80.0" + "@aws-sdk/node-http-handler" "3.82.0" + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/smithy-client" "3.85.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/url-parser" "3.78.0" + "@aws-sdk/util-base64-browser" "3.58.0" + "@aws-sdk/util-base64-node" "3.55.0" + "@aws-sdk/util-body-length-browser" "3.55.0" + "@aws-sdk/util-body-length-node" "3.55.0" + "@aws-sdk/util-defaults-mode-browser" "3.85.0" + "@aws-sdk/util-defaults-mode-node" "3.85.0" + "@aws-sdk/util-user-agent-browser" "3.78.0" + "@aws-sdk/util-user-agent-node" "3.80.0" + "@aws-sdk/util-utf8-browser" "3.55.0" + "@aws-sdk/util-utf8-node" "3.55.0" + tslib "^2.3.1" + uuid "^8.3.2" + +"@aws-sdk/client-codepipeline@3.92.0": + version "3.92.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-codepipeline/-/client-codepipeline-3.92.0.tgz#7143cc6c0859a2a082487b0bd87653ffc159daba" + integrity sha512-uP/8qN+qliwawp0tbXoToa6EeZMLxgtFWV0ZRU6rBVIz7PScWrrtcyJb8yCMGo0d80R+SRv4z2rWhpIsCrOF8w== + dependencies: + "@aws-crypto/sha256-browser" "2.0.0" + "@aws-crypto/sha256-js" "2.0.0" + "@aws-sdk/client-sts" "3.92.0" + "@aws-sdk/config-resolver" "3.80.0" + "@aws-sdk/credential-provider-node" "3.87.0" + "@aws-sdk/fetch-http-handler" "3.78.0" + "@aws-sdk/hash-node" "3.78.0" + "@aws-sdk/invalid-dependency" "3.78.0" + "@aws-sdk/middleware-content-length" "3.78.0" + "@aws-sdk/middleware-host-header" "3.78.0" + "@aws-sdk/middleware-logger" "3.78.0" + "@aws-sdk/middleware-retry" "3.80.0" + "@aws-sdk/middleware-serde" "3.78.0" + "@aws-sdk/middleware-signing" "3.78.0" + "@aws-sdk/middleware-stack" "3.78.0" + "@aws-sdk/middleware-user-agent" "3.78.0" + "@aws-sdk/node-config-provider" "3.80.0" + "@aws-sdk/node-http-handler" "3.82.0" + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/smithy-client" "3.85.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/url-parser" "3.78.0" + "@aws-sdk/util-base64-browser" "3.58.0" + "@aws-sdk/util-base64-node" "3.55.0" + "@aws-sdk/util-body-length-browser" "3.55.0" + "@aws-sdk/util-body-length-node" "3.55.0" + "@aws-sdk/util-defaults-mode-browser" "3.85.0" + "@aws-sdk/util-defaults-mode-node" "3.85.0" + "@aws-sdk/util-user-agent-browser" "3.78.0" + "@aws-sdk/util-user-agent-node" "3.80.0" + "@aws-sdk/util-utf8-browser" "3.55.0" + "@aws-sdk/util-utf8-node" "3.55.0" + tslib "^2.3.1" + uuid "^8.3.2" + +"@aws-sdk/client-dynamodb@3.87.0": + version "3.87.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-dynamodb/-/client-dynamodb-3.87.0.tgz#9358fef700b623ccfb016a67a0acc6f3519725ff" + integrity sha512-BvyHQ0cigzGzSUUYsf8ckTU7wc/n3My3lu/iQHWZhVecu7huBa01qExRBNNhC/n2KmNjRxImP3ek8ccnR0HNzQ== + dependencies: + "@aws-crypto/sha256-browser" "2.0.0" + "@aws-crypto/sha256-js" "2.0.0" + "@aws-sdk/client-sts" "3.87.0" + "@aws-sdk/config-resolver" "3.80.0" + "@aws-sdk/credential-provider-node" "3.87.0" + "@aws-sdk/fetch-http-handler" "3.78.0" + "@aws-sdk/hash-node" "3.78.0" + "@aws-sdk/invalid-dependency" "3.78.0" + "@aws-sdk/middleware-content-length" "3.78.0" + "@aws-sdk/middleware-endpoint-discovery" "3.80.0" + "@aws-sdk/middleware-host-header" "3.78.0" + "@aws-sdk/middleware-logger" "3.78.0" + "@aws-sdk/middleware-retry" "3.80.0" + "@aws-sdk/middleware-serde" "3.78.0" + "@aws-sdk/middleware-signing" "3.78.0" + "@aws-sdk/middleware-stack" "3.78.0" + "@aws-sdk/middleware-user-agent" "3.78.0" + "@aws-sdk/node-config-provider" "3.80.0" + "@aws-sdk/node-http-handler" "3.82.0" + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/smithy-client" "3.85.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/url-parser" "3.78.0" + "@aws-sdk/util-base64-browser" "3.58.0" + "@aws-sdk/util-base64-node" "3.55.0" + "@aws-sdk/util-body-length-browser" "3.55.0" + "@aws-sdk/util-body-length-node" "3.55.0" + "@aws-sdk/util-defaults-mode-browser" "3.85.0" + "@aws-sdk/util-defaults-mode-node" "3.85.0" + "@aws-sdk/util-user-agent-browser" "3.78.0" + "@aws-sdk/util-user-agent-node" "3.80.0" + "@aws-sdk/util-utf8-browser" "3.55.0" + "@aws-sdk/util-utf8-node" "3.55.0" + "@aws-sdk/util-waiter" "3.78.0" + tslib "^2.3.1" + uuid "^8.3.2" + +"@aws-sdk/client-dynamodb@3.92.0": + version "3.92.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-dynamodb/-/client-dynamodb-3.92.0.tgz#0a58fd9bf808ce079eaee2d639f558cd4c47eed6" + integrity sha512-IknNC9G2pfXjdCTZWUeFsoD444pj3uUEtlYUa/h3wkXFkcOuNdNh3u9ZWp4DY5WiyNQglrkm4qRkmyVZBBmtoA== + dependencies: + "@aws-crypto/sha256-browser" "2.0.0" + "@aws-crypto/sha256-js" "2.0.0" + "@aws-sdk/client-sts" "3.92.0" + "@aws-sdk/config-resolver" "3.80.0" + "@aws-sdk/credential-provider-node" "3.87.0" + "@aws-sdk/fetch-http-handler" "3.78.0" + "@aws-sdk/hash-node" "3.78.0" + "@aws-sdk/invalid-dependency" "3.78.0" + "@aws-sdk/middleware-content-length" "3.78.0" + "@aws-sdk/middleware-endpoint-discovery" "3.80.0" + "@aws-sdk/middleware-host-header" "3.78.0" + "@aws-sdk/middleware-logger" "3.78.0" + "@aws-sdk/middleware-retry" "3.80.0" + "@aws-sdk/middleware-serde" "3.78.0" + "@aws-sdk/middleware-signing" "3.78.0" + "@aws-sdk/middleware-stack" "3.78.0" + "@aws-sdk/middleware-user-agent" "3.78.0" + "@aws-sdk/node-config-provider" "3.80.0" + "@aws-sdk/node-http-handler" "3.82.0" + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/smithy-client" "3.85.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/url-parser" "3.78.0" + "@aws-sdk/util-base64-browser" "3.58.0" + "@aws-sdk/util-base64-node" "3.55.0" + "@aws-sdk/util-body-length-browser" "3.55.0" + "@aws-sdk/util-body-length-node" "3.55.0" + "@aws-sdk/util-defaults-mode-browser" "3.85.0" + "@aws-sdk/util-defaults-mode-node" "3.85.0" + "@aws-sdk/util-user-agent-browser" "3.78.0" + "@aws-sdk/util-user-agent-node" "3.80.0" + "@aws-sdk/util-utf8-browser" "3.55.0" + "@aws-sdk/util-utf8-node" "3.55.0" + "@aws-sdk/util-waiter" "3.78.0" + tslib "^2.3.1" + uuid "^8.3.2" + +"@aws-sdk/client-kms@3.92.0": + version "3.92.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-kms/-/client-kms-3.92.0.tgz#a7a4426b7fa9da1992b3d5d2abb7158c96fd8179" + integrity sha512-ZSFwgGKVtFgmqn15d65BpoQ1meMnKqvzkPSzJMRfKnLhvWThYHW930rdrT91q3sWh6SpjYtUZnmtX/ddDhY10w== + dependencies: + "@aws-crypto/sha256-browser" "2.0.0" + "@aws-crypto/sha256-js" "2.0.0" + "@aws-sdk/client-sts" "3.92.0" + "@aws-sdk/config-resolver" "3.80.0" + "@aws-sdk/credential-provider-node" "3.87.0" + "@aws-sdk/fetch-http-handler" "3.78.0" + "@aws-sdk/hash-node" "3.78.0" + "@aws-sdk/invalid-dependency" "3.78.0" + "@aws-sdk/middleware-content-length" "3.78.0" + "@aws-sdk/middleware-host-header" "3.78.0" + "@aws-sdk/middleware-logger" "3.78.0" + "@aws-sdk/middleware-retry" "3.80.0" + "@aws-sdk/middleware-serde" "3.78.0" + "@aws-sdk/middleware-signing" "3.78.0" + "@aws-sdk/middleware-stack" "3.78.0" + "@aws-sdk/middleware-user-agent" "3.78.0" + "@aws-sdk/node-config-provider" "3.80.0" + "@aws-sdk/node-http-handler" "3.82.0" + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/smithy-client" "3.85.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/url-parser" "3.78.0" + "@aws-sdk/util-base64-browser" "3.58.0" + "@aws-sdk/util-base64-node" "3.55.0" + "@aws-sdk/util-body-length-browser" "3.55.0" + "@aws-sdk/util-body-length-node" "3.55.0" + "@aws-sdk/util-defaults-mode-browser" "3.85.0" + "@aws-sdk/util-defaults-mode-node" "3.85.0" + "@aws-sdk/util-user-agent-browser" "3.78.0" + "@aws-sdk/util-user-agent-node" "3.80.0" + "@aws-sdk/util-utf8-browser" "3.55.0" + "@aws-sdk/util-utf8-node" "3.55.0" + tslib "^2.3.1" + +"@aws-sdk/client-organizations@3.92.0": + version "3.92.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-organizations/-/client-organizations-3.92.0.tgz#fe6b62ad2f22eb739c41556b3ef9ba196889672b" + integrity sha512-TRVdI2eKWjy6Do4f27BMFbLw0dx4BSLsyEVkmkTmnd9blDC0JzgROq9dh0umeSZeg+/bpmvWiq6tV0FzmeumdA== + dependencies: + "@aws-crypto/sha256-browser" "2.0.0" + "@aws-crypto/sha256-js" "2.0.0" + "@aws-sdk/client-sts" "3.92.0" + "@aws-sdk/config-resolver" "3.80.0" + "@aws-sdk/credential-provider-node" "3.87.0" + "@aws-sdk/fetch-http-handler" "3.78.0" + "@aws-sdk/hash-node" "3.78.0" + "@aws-sdk/invalid-dependency" "3.78.0" + "@aws-sdk/middleware-content-length" "3.78.0" + "@aws-sdk/middleware-host-header" "3.78.0" + "@aws-sdk/middleware-logger" "3.78.0" + "@aws-sdk/middleware-retry" "3.80.0" + "@aws-sdk/middleware-serde" "3.78.0" + "@aws-sdk/middleware-signing" "3.78.0" + "@aws-sdk/middleware-stack" "3.78.0" + "@aws-sdk/middleware-user-agent" "3.78.0" + "@aws-sdk/node-config-provider" "3.80.0" + "@aws-sdk/node-http-handler" "3.82.0" + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/smithy-client" "3.85.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/url-parser" "3.78.0" + "@aws-sdk/util-base64-browser" "3.58.0" + "@aws-sdk/util-base64-node" "3.55.0" + "@aws-sdk/util-body-length-browser" "3.55.0" + "@aws-sdk/util-body-length-node" "3.55.0" + "@aws-sdk/util-defaults-mode-browser" "3.85.0" + "@aws-sdk/util-defaults-mode-node" "3.85.0" + "@aws-sdk/util-user-agent-browser" "3.78.0" + "@aws-sdk/util-user-agent-node" "3.80.0" + "@aws-sdk/util-utf8-browser" "3.55.0" + "@aws-sdk/util-utf8-node" "3.55.0" + tslib "^2.3.1" + +"@aws-sdk/client-s3@3.92.0": + version "3.92.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-s3/-/client-s3-3.92.0.tgz#51da35f72278982168b7ab7121b1ca91957742a0" + integrity sha512-3gjOTuaQEBnzRPqNGXQFs3POIWlifo6d7N95p67kKEcxELP/5q2c56kzntfYwQ8lPDzfemzwRATltVBTV0bsBg== + dependencies: + "@aws-crypto/sha1-browser" "2.0.0" + "@aws-crypto/sha256-browser" "2.0.0" + "@aws-crypto/sha256-js" "2.0.0" + "@aws-sdk/client-sts" "3.92.0" + "@aws-sdk/config-resolver" "3.80.0" + "@aws-sdk/credential-provider-node" "3.87.0" + "@aws-sdk/eventstream-serde-browser" "3.78.0" + "@aws-sdk/eventstream-serde-config-resolver" "3.78.0" + "@aws-sdk/eventstream-serde-node" "3.78.0" + "@aws-sdk/fetch-http-handler" "3.78.0" + "@aws-sdk/hash-blob-browser" "3.78.0" + "@aws-sdk/hash-node" "3.78.0" + "@aws-sdk/hash-stream-node" "3.78.0" + "@aws-sdk/invalid-dependency" "3.78.0" + "@aws-sdk/md5-js" "3.78.0" + "@aws-sdk/middleware-bucket-endpoint" "3.80.0" + "@aws-sdk/middleware-content-length" "3.78.0" + "@aws-sdk/middleware-expect-continue" "3.78.0" + "@aws-sdk/middleware-flexible-checksums" "3.78.0" + "@aws-sdk/middleware-host-header" "3.78.0" + "@aws-sdk/middleware-location-constraint" "3.78.0" + "@aws-sdk/middleware-logger" "3.78.0" + "@aws-sdk/middleware-retry" "3.80.0" + "@aws-sdk/middleware-sdk-s3" "3.86.0" + "@aws-sdk/middleware-serde" "3.78.0" + "@aws-sdk/middleware-signing" "3.78.0" + "@aws-sdk/middleware-ssec" "3.78.0" + "@aws-sdk/middleware-stack" "3.78.0" + "@aws-sdk/middleware-user-agent" "3.78.0" + "@aws-sdk/node-config-provider" "3.80.0" + "@aws-sdk/node-http-handler" "3.82.0" + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/signature-v4-multi-region" "3.88.0" + "@aws-sdk/smithy-client" "3.85.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/url-parser" "3.78.0" + "@aws-sdk/util-base64-browser" "3.58.0" + "@aws-sdk/util-base64-node" "3.55.0" + "@aws-sdk/util-body-length-browser" "3.55.0" + "@aws-sdk/util-body-length-node" "3.55.0" + "@aws-sdk/util-defaults-mode-browser" "3.85.0" + "@aws-sdk/util-defaults-mode-node" "3.85.0" + "@aws-sdk/util-stream-browser" "3.78.0" + "@aws-sdk/util-stream-node" "3.78.0" + "@aws-sdk/util-user-agent-browser" "3.78.0" + "@aws-sdk/util-user-agent-node" "3.80.0" + "@aws-sdk/util-utf8-browser" "3.55.0" + "@aws-sdk/util-utf8-node" "3.55.0" + "@aws-sdk/util-waiter" "3.78.0" + "@aws-sdk/xml-builder" "3.55.0" + entities "2.2.0" + fast-xml-parser "3.19.0" + tslib "^2.3.1" + +"@aws-sdk/client-sso@3.85.0": + version "3.85.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso/-/client-sso-3.85.0.tgz#4e5cf2b9e9898ff23c0aed1af0bac8d46ceed229" + integrity sha512-JMW0NzFpo99oE6O9M/kgLela73p4vmhe/5TIcdrqUvP9XUV9nANl5nSXh3rqLz0ubmliedz9kdYYhwMC3ntoXg== + dependencies: + "@aws-crypto/sha256-browser" "2.0.0" + "@aws-crypto/sha256-js" "2.0.0" + "@aws-sdk/config-resolver" "3.80.0" + "@aws-sdk/fetch-http-handler" "3.78.0" + "@aws-sdk/hash-node" "3.78.0" + "@aws-sdk/invalid-dependency" "3.78.0" + "@aws-sdk/middleware-content-length" "3.78.0" + "@aws-sdk/middleware-host-header" "3.78.0" + "@aws-sdk/middleware-logger" "3.78.0" + "@aws-sdk/middleware-retry" "3.80.0" + "@aws-sdk/middleware-serde" "3.78.0" + "@aws-sdk/middleware-stack" "3.78.0" + "@aws-sdk/middleware-user-agent" "3.78.0" + "@aws-sdk/node-config-provider" "3.80.0" + "@aws-sdk/node-http-handler" "3.82.0" + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/smithy-client" "3.85.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/url-parser" "3.78.0" + "@aws-sdk/util-base64-browser" "3.58.0" + "@aws-sdk/util-base64-node" "3.55.0" + "@aws-sdk/util-body-length-browser" "3.55.0" + "@aws-sdk/util-body-length-node" "3.55.0" + "@aws-sdk/util-defaults-mode-browser" "3.85.0" + "@aws-sdk/util-defaults-mode-node" "3.85.0" + "@aws-sdk/util-user-agent-browser" "3.78.0" + "@aws-sdk/util-user-agent-node" "3.80.0" + "@aws-sdk/util-utf8-browser" "3.55.0" + "@aws-sdk/util-utf8-node" "3.55.0" + tslib "^2.3.1" + +"@aws-sdk/client-sts@3.87.0": + version "3.87.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sts/-/client-sts-3.87.0.tgz#65c18dce2ba8312a8cb4289b29bc1f507db97e92" + integrity sha512-JGI5rzSq8T7IVlfDJ8ltGl8nyVEtwvqXrYR87DwTjeE4HP+/oBdWdbO0oBL1TJMGjzZcENyVYvmaSAkobenkTg== + dependencies: + "@aws-crypto/sha256-browser" "2.0.0" + "@aws-crypto/sha256-js" "2.0.0" + "@aws-sdk/config-resolver" "3.80.0" + "@aws-sdk/credential-provider-node" "3.87.0" + "@aws-sdk/fetch-http-handler" "3.78.0" + "@aws-sdk/hash-node" "3.78.0" + "@aws-sdk/invalid-dependency" "3.78.0" + "@aws-sdk/middleware-content-length" "3.78.0" + "@aws-sdk/middleware-host-header" "3.78.0" + "@aws-sdk/middleware-logger" "3.78.0" + "@aws-sdk/middleware-retry" "3.80.0" + "@aws-sdk/middleware-sdk-sts" "3.78.0" + "@aws-sdk/middleware-serde" "3.78.0" + "@aws-sdk/middleware-signing" "3.78.0" + "@aws-sdk/middleware-stack" "3.78.0" + "@aws-sdk/middleware-user-agent" "3.78.0" + "@aws-sdk/node-config-provider" "3.80.0" + "@aws-sdk/node-http-handler" "3.82.0" + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/smithy-client" "3.85.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/url-parser" "3.78.0" + "@aws-sdk/util-base64-browser" "3.58.0" + "@aws-sdk/util-base64-node" "3.55.0" + "@aws-sdk/util-body-length-browser" "3.55.0" + "@aws-sdk/util-body-length-node" "3.55.0" + "@aws-sdk/util-defaults-mode-browser" "3.85.0" + "@aws-sdk/util-defaults-mode-node" "3.85.0" + "@aws-sdk/util-user-agent-browser" "3.78.0" + "@aws-sdk/util-user-agent-node" "3.80.0" + "@aws-sdk/util-utf8-browser" "3.55.0" + "@aws-sdk/util-utf8-node" "3.55.0" + entities "2.2.0" + fast-xml-parser "3.19.0" + tslib "^2.3.1" + +"@aws-sdk/client-sts@3.92.0": + version "3.92.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sts/-/client-sts-3.92.0.tgz#717bfb5b723e1d25516e1f8c9cee1f78746b0f70" + integrity sha512-ux9lg3tKVZasaD43WKTSep8bNsf4fBs2MsryxLHwPcxQc28ANKWsVbDFSz7clvorO3a0kKasg7d3HG9N7S+Xlg== + dependencies: + "@aws-crypto/sha256-browser" "2.0.0" + "@aws-crypto/sha256-js" "2.0.0" + "@aws-sdk/config-resolver" "3.80.0" + "@aws-sdk/credential-provider-node" "3.87.0" + "@aws-sdk/fetch-http-handler" "3.78.0" + "@aws-sdk/hash-node" "3.78.0" + "@aws-sdk/invalid-dependency" "3.78.0" + "@aws-sdk/middleware-content-length" "3.78.0" + "@aws-sdk/middleware-host-header" "3.78.0" + "@aws-sdk/middleware-logger" "3.78.0" + "@aws-sdk/middleware-retry" "3.80.0" + "@aws-sdk/middleware-sdk-sts" "3.78.0" + "@aws-sdk/middleware-serde" "3.78.0" + "@aws-sdk/middleware-signing" "3.78.0" + "@aws-sdk/middleware-stack" "3.78.0" + "@aws-sdk/middleware-user-agent" "3.78.0" + "@aws-sdk/node-config-provider" "3.80.0" + "@aws-sdk/node-http-handler" "3.82.0" + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/smithy-client" "3.85.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/url-parser" "3.78.0" + "@aws-sdk/util-base64-browser" "3.58.0" + "@aws-sdk/util-base64-node" "3.55.0" + "@aws-sdk/util-body-length-browser" "3.55.0" + "@aws-sdk/util-body-length-node" "3.55.0" + "@aws-sdk/util-defaults-mode-browser" "3.85.0" + "@aws-sdk/util-defaults-mode-node" "3.85.0" + "@aws-sdk/util-user-agent-browser" "3.78.0" + "@aws-sdk/util-user-agent-node" "3.80.0" + "@aws-sdk/util-utf8-browser" "3.55.0" + "@aws-sdk/util-utf8-node" "3.55.0" + entities "2.2.0" + fast-xml-parser "3.19.0" + tslib "^2.3.1" + +"@aws-sdk/config-resolver@3.80.0": + version "3.80.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/config-resolver/-/config-resolver-3.80.0.tgz#a804aba4d4767402ab15640757c8c8bb2254eec1" + integrity sha512-vFruNKlmhsaC8yjnHmasi1WW/7EELlEuFTj4mqcqNqR4dfraf0maVvpqF1VSR8EstpFMsGYI5dmoWAnnG4PcLQ== + dependencies: + "@aws-sdk/signature-v4" "3.78.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/util-config-provider" "3.55.0" + "@aws-sdk/util-middleware" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/credential-provider-env@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.78.0.tgz#e3013073bab0db313b0505d790aa79a35bd582d9" + integrity sha512-K41VTIzVHm2RyIwtBER8Hte3huUBXdV1WKO+i7olYVgLFmaqcZUNrlyoGDRqZcQ/u4AbxTzBU9jeMIbIfzMOWg== + dependencies: + "@aws-sdk/property-provider" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/credential-provider-imds@3.81.0": + version "3.81.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-imds/-/credential-provider-imds-3.81.0.tgz#1ffd1219b7fd19eec4d4d4b5b06bda66e3bc210e" + integrity sha512-BHopP+gaovTYj+4tSrwCk8NNCR48gE9CWmpIOLkP9ell0gOL81Qh7aCEiIK0BZBZkccv1s16cYq1MSZZGS7PEQ== + dependencies: + "@aws-sdk/node-config-provider" "3.80.0" + "@aws-sdk/property-provider" "3.78.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/url-parser" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/credential-provider-ini@3.85.0": + version "3.85.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.85.0.tgz#ecbd1d9f3afbcb054b241ae5ced0bc5db6b2a053" + integrity sha512-KgzLGq+w8OrSLutwdYUw0POeLinGQKcqvQJ9702eoeXCwZMnEHwKqU61bn8QKMX/tuYVCNV4I1enI7MmYPW8Lw== + dependencies: + "@aws-sdk/credential-provider-env" "3.78.0" + "@aws-sdk/credential-provider-imds" "3.81.0" + "@aws-sdk/credential-provider-sso" "3.85.0" + "@aws-sdk/credential-provider-web-identity" "3.78.0" + "@aws-sdk/property-provider" "3.78.0" + "@aws-sdk/shared-ini-file-loader" "3.80.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/credential-provider-node@3.87.0": + version "3.87.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.87.0.tgz#700e328ac21219cac521e119d06dead873c5ada1" + integrity sha512-yL9W5nX00grNNsGj2df1y7hQ0F77UA7+2toPOVqYPIDhFtIUA97AVYiBEFQz1mO9OAhUfCGgxuFF4pyqFoMcHQ== + dependencies: + "@aws-sdk/credential-provider-env" "3.78.0" + "@aws-sdk/credential-provider-imds" "3.81.0" + "@aws-sdk/credential-provider-ini" "3.85.0" + "@aws-sdk/credential-provider-process" "3.80.0" + "@aws-sdk/credential-provider-sso" "3.85.0" + "@aws-sdk/credential-provider-web-identity" "3.78.0" + "@aws-sdk/property-provider" "3.78.0" + "@aws-sdk/shared-ini-file-loader" "3.80.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/credential-provider-process@3.80.0": + version "3.80.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.80.0.tgz#625577774278f845fe5bd0f311ed53973ec92ede" + integrity sha512-3Ro+kMMyLUJHefOhGc5pOO/ibGcJi8bkj0z/Jtqd5I2Sm1qi7avoztST67/k48KMW1OqPnD/FUqxz5T8B2d+FQ== + dependencies: + "@aws-sdk/property-provider" "3.78.0" + "@aws-sdk/shared-ini-file-loader" "3.80.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/credential-provider-sso@3.85.0": + version "3.85.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.85.0.tgz#05f0d4b004d0a6ff799c09f0923ae4d4c55f2c9a" + integrity sha512-uE238BgJ/AftPDlBGDlV0XdiNWnUZxFmUmLxgbr19/6jHaCuBr//T6rP+Bc0BjcHkvQCvTdFoCjs17R3Quy3cw== + dependencies: + "@aws-sdk/client-sso" "3.85.0" + "@aws-sdk/property-provider" "3.78.0" + "@aws-sdk/shared-ini-file-loader" "3.80.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/credential-provider-web-identity@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.78.0.tgz#61cc6c5c065de3d8d34b7633899e3bbfa9a24c9d" + integrity sha512-9/IvqHdJaVqMEABA8xZE3t5YF1S2PepfckVu0Ws9YUglj6oO+2QyVX6aRgMF1xph6781+Yc31TDh8/3eaDja7w== + dependencies: + "@aws-sdk/property-provider" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/endpoint-cache@3.55.0": + version "3.55.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/endpoint-cache/-/endpoint-cache-3.55.0.tgz#b4be2cb363af005a67c2a6f938f06fcbbbbbccf8" + integrity sha512-kxDoHFDuQwZEEUZRp+ZLOg68EXuKPzUN86DcpIZantDVcmu7MSPTbbQp9DZd8MnKVEKCP7Sop5f7zCqOPl3LXw== + dependencies: + mnemonist "0.38.3" + tslib "^2.3.1" + +"@aws-sdk/eventstream-marshaller@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-marshaller/-/eventstream-marshaller-3.78.0.tgz#32df7136d644d0d91a563a9a192b6e2d4df873d0" + integrity sha512-BMbRvLe6wNWQ+NO1pdPw3kGXXEdYV94BxEr3rTkKwr5yHpl8sUb/Va9sJJufUjzggpgE4vYu5nVsrT8ByMYXuA== + dependencies: + "@aws-crypto/crc32" "2.0.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/util-hex-encoding" "3.58.0" + tslib "^2.3.1" + +"@aws-sdk/eventstream-serde-browser@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-serde-browser/-/eventstream-serde-browser-3.78.0.tgz#27b019f6f17a54e18cd44041b29ef234cc04f545" + integrity sha512-ehQI2iLsj8MMskDRbrPB7SibIdJq6LleBP6ojT+cgrLJRbVXUOxK+3MPHDZVdGYx4ukVg48E1fA2DzVfAp7Emw== + dependencies: + "@aws-sdk/eventstream-marshaller" "3.78.0" + "@aws-sdk/eventstream-serde-universal" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/eventstream-serde-config-resolver@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-3.78.0.tgz#ea6d24d763413bc53da6230e06660382ba94a40c" + integrity sha512-iUG0wtZH/L7d6XfipwbhgjBHip0uTm9S27EasCn+g0CunbW6w7rXd7rfMqA+gSLVXPTBYjTMPIwRxrTCdRprwA== + dependencies: + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/eventstream-serde-node@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-serde-node/-/eventstream-serde-node-3.78.0.tgz#138d99043b11b7cdfd63425b257fae64ec404374" + integrity sha512-H78LLoZEngZBSdk3lRQkAaR3cGsy/3UIjq9AFPeqoPVQtHkzBob1jVfE/5VSVAMhKLxWn8iqhRPS37AvyBGOwQ== + dependencies: + "@aws-sdk/eventstream-marshaller" "3.78.0" + "@aws-sdk/eventstream-serde-universal" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/eventstream-serde-universal@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-serde-universal/-/eventstream-serde-universal-3.78.0.tgz#9d7f3caf83cdc89ca7e3cf3a24734b0bbf43c81c" + integrity sha512-PZTLdyF923/1GJuMNtq9VMGd2vEx33HhsGInXvYtulKDSD5SgaTGj+Dz5wYepqL1gUEuXqZjBD71uZgrY/JgRg== + dependencies: + "@aws-sdk/eventstream-marshaller" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/fetch-http-handler@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/fetch-http-handler/-/fetch-http-handler-3.78.0.tgz#9cd4a02eaf015b4a5a18552e8c9e8fbfce7219a3" + integrity sha512-cR6r2h2kJ1DNEZSXC6GknQB7OKmy+s9ZNV+g3AsNqkrUmNNOaHpFoSn+m6SC3qaclcGd0eQBpqzSu/TDn23Ihw== + dependencies: + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/querystring-builder" "3.78.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/util-base64-browser" "3.58.0" + tslib "^2.3.1" + +"@aws-sdk/hash-blob-browser@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/hash-blob-browser/-/hash-blob-browser-3.78.0.tgz#6f774f58c59bb02749b7239a35be9cf61cc8520e" + integrity sha512-IEkA+t6qJEtEYEZgsqFRRITeZJ3mirw7IHJVHxwb86lpeufTVcbILI59B8/rhbqG+9dk0kWTjYSjC/ZdM+rgHA== + dependencies: + "@aws-sdk/chunked-blob-reader" "3.55.0" + "@aws-sdk/chunked-blob-reader-native" "3.58.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/hash-node@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/hash-node/-/hash-node-3.78.0.tgz#d03f804a685bc1cea9df3eabf499b2a7659d01fd" + integrity sha512-ev48yXaqZVtMeuKy52LUZPHCyKvkKQ9uiUebqkA+zFxIk+eN8SMPFHmsififIHWuS6ZkXBUSctjH9wmLebH60A== + dependencies: + "@aws-sdk/types" "3.78.0" + "@aws-sdk/util-buffer-from" "3.55.0" + tslib "^2.3.1" + +"@aws-sdk/hash-stream-node@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/hash-stream-node/-/hash-stream-node-3.78.0.tgz#7b321a4ab4384bd51f19e626e5dae111b8fac4dd" + integrity sha512-y42Pm0Nk6zf/MI6acLFVFAMya0Ncvy6F6Xu5aYAmwIMIoMI0ctNeyuL/Dikgt8+oyxC+kORw+W9jtzgWj2zY/w== + dependencies: + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/invalid-dependency@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/invalid-dependency/-/invalid-dependency-3.78.0.tgz#c4e30871d69894dbf3450023319385110ce95c81" + integrity sha512-zUo+PbeRMN/Mzj6y+6p9qqk/znuFetT1gmpOcZGL9Rp2T+b9WJWd+daq5ktsL10sVCzIt2UvneJRz6b+aU+bfw== + dependencies: + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/is-array-buffer@3.55.0": + version "3.55.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/is-array-buffer/-/is-array-buffer-3.55.0.tgz#c46122c5636f01d5895e5256a587768c3425ea7a" + integrity sha512-NbiPHVYuPxdqdFd6FxzzN3H1BQn/iWA3ri3Ry7AyLeP/tGs1yzEWMwf8BN8TSMALI0GXT6Sh0GDWy3Ok5xB6DA== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/lib-dynamodb@3.87.0": + version "3.87.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/lib-dynamodb/-/lib-dynamodb-3.87.0.tgz#e45fb1801ab74c3df3e4685f1979edd411e17aaa" + integrity sha512-yOTj0AeS7QunCYbwEwLRuxyqxSpN7TsuuUETkWJnq3xcbQL2/RchW//ZOtt0mB+3sAJJDaXlkYn4tpI8hgEB4g== + dependencies: + "@aws-sdk/util-dynamodb" "3.87.0" + tslib "^2.3.1" + +"@aws-sdk/lib-dynamodb@3.92.0": + version "3.92.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/lib-dynamodb/-/lib-dynamodb-3.92.0.tgz#c20009bf1758058459d83b99b69c1ec7efa967fa" + integrity sha512-2srcul4EcsH6q7aeVM76VFouPZlU7IQ8Z3Oibig897GO/2EMSA1rWM3QnrvqDRfZwH7xJUuaghvajO+zT5EVJQ== + dependencies: + "@aws-sdk/util-dynamodb" "3.92.0" + tslib "^2.3.1" + +"@aws-sdk/md5-js@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/md5-js/-/md5-js-3.78.0.tgz#a79357e6518778057b7bcbbd45dcb352be5f8e15" + integrity sha512-vKOXJWJvv6QH6rnqMYEWzwAnMr4hfcmY8+t6BAuTcDpcEVF77e3bwUcaajXi2U0JMuNvnLwuJF3h6kL6aX4l6g== + dependencies: + "@aws-sdk/types" "3.78.0" + "@aws-sdk/util-utf8-browser" "3.55.0" + "@aws-sdk/util-utf8-node" "3.55.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-bucket-endpoint@3.80.0": + version "3.80.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.80.0.tgz#0632e94900472eb86d0cfdf521251a1bdefba843" + integrity sha512-FSSx6IgT7xftSlpjxoPKv8XI9nv7EK+OCODo2s3CmElMW1kBRdmQ/ImVuTwvqhdxJEVUeUdgupmC7cqyqgt04w== + dependencies: + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/util-arn-parser" "3.55.0" + "@aws-sdk/util-config-provider" "3.55.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-content-length@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-content-length/-/middleware-content-length-3.78.0.tgz#57d46be61d1176d4c5fce7ba4b0682798c170208" + integrity sha512-5MpKt6lB9TdFy25/AGrpOjPY0iDHZAKpEHc+jSOJBXLl6xunXA7qHdiYaVqkWodLxy70nIckGNHqQ3drabidkA== + dependencies: + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-endpoint-discovery@3.80.0": + version "3.80.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-endpoint-discovery/-/middleware-endpoint-discovery-3.80.0.tgz#4a96324020efb24eec75f6207949119c98e097a8" + integrity sha512-73pKz8ossZKisG684raP1dn2u3fQRktWY29oa9Q3cBvRYdyu5UOhwayt2MObgSC8S6NfNdTGC/DGf7+/JRSY7A== + dependencies: + "@aws-sdk/config-resolver" "3.80.0" + "@aws-sdk/endpoint-cache" "3.55.0" + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-expect-continue@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.78.0.tgz#35df662ecf31a1c8540781154f514615f3ca2c97" + integrity sha512-IXfcSugFV3uNk50VQsN/Cm80iCsUSwcYJ5RzEwy7wXbZ+KM03xWXlbXzqkeTDnS74wLWSw09nKF3rkp1eyfDfg== + dependencies: + "@aws-sdk/middleware-header-default" "3.78.0" + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-flexible-checksums@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.78.0.tgz#9128b0acb5d9df0f0e0ef06cb1d17a44afe650fc" + integrity sha512-1jjxHcB3Le/2Z7BzugXzZnIwKGlUluNm0d1lB4fF2QVq3GHlA6e8uv0rCtqe/3wSsrzV6YzJ8vjioymKSNIjKQ== + dependencies: + "@aws-crypto/crc32" "2.0.0" + "@aws-crypto/crc32c" "2.0.0" + "@aws-sdk/is-array-buffer" "3.55.0" + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-header-default@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-header-default/-/middleware-header-default-3.78.0.tgz#911b7f6ce4b4ae45ab032e32768d527ca6ae1d6c" + integrity sha512-USyOIF7ObBVMKbV/8lOBLDNwMAGdOtujd+RO/9dX6OQLceUTKIS1dOfJoYYwRHgengn7ikpDxoyROyspPYYDZQ== + dependencies: + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-host-header@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-host-header/-/middleware-host-header-3.78.0.tgz#9130d176c2839bc658aff01bf2a36fee705f0e86" + integrity sha512-1zL8uaDWGmH50c8B8jjz75e0ePj6/3QeZEhjJgTgL6DTdiqvRt32p3t+XWHW+yDI14fZZUYeTklAaLVxqFrHqQ== + dependencies: + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-location-constraint@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.78.0.tgz#f3af44e443a0993e413a787a446bb6693b5b0e7e" + integrity sha512-m626H1WwXYJtwHEkV/2DsLlu1ckWq3j57NzsexZki3qS0nU8HEiDl6YYi+k84vDD4Qpba6EI9AdhzwnvZLXtGw== + dependencies: + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-logger@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-logger/-/middleware-logger-3.78.0.tgz#758b84711213b2e78afe0df20bc2d4d70a856da1" + integrity sha512-GBhwxNjhCJUIeQQDaGasX/C23Jay77al2vRyGwmxf8no0DdFsa4J1Ik6/2hhIqkqko+WM4SpCnpZrY4MtnxNvA== + dependencies: + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-retry@3.80.0": + version "3.80.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-retry/-/middleware-retry-3.80.0.tgz#d62ebd68ded78bdaf0a8b07bb4cc1c394c99cc8f" + integrity sha512-CTk+tA4+WMUNOcUfR6UQrkhwvPYFpnMsQ1vuHlpLFOGG3nCqywA2hueLMRQmVcDXzP0sGeygce6dzRI9dJB/GA== + dependencies: + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/service-error-classification" "3.78.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/util-middleware" "3.78.0" + tslib "^2.3.1" + uuid "^8.3.2" + +"@aws-sdk/middleware-sdk-s3@3.86.0": + version "3.86.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.86.0.tgz#92470446c62eec1cbe502e4f04585194bb5b1ff7" + integrity sha512-1L9q8iJXy/KNyVR8JRs4DZ5SJse6nJPiK4AR8c2xF5FWHdGoFaLcdqpg2/TLB1kpdcfGgNp96uCROxh+IPXtDQ== + dependencies: + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/util-arn-parser" "3.55.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-sdk-sts@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.78.0.tgz#15d91c421380f748b58bb006e1c398cfdf59b290" + integrity sha512-Lu/kN0J0/Kt0ON1hvwNel+y8yvf35licfIgtedHbBCa/ju8qQ9j+uL9Lla6Y5Tqu29yVaye1JxhiIDhscSwrLA== + dependencies: + "@aws-sdk/middleware-signing" "3.78.0" + "@aws-sdk/property-provider" "3.78.0" + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/signature-v4" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-serde@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-serde/-/middleware-serde-3.78.0.tgz#d1e1a7b9ac58638b973e533ac4c2ca52f413883c" + integrity sha512-4DPsNOxsl1bxRzfo1WXEZjmD7OEi7qGNpxrDWucVe96Fqj2dH08jR8wxvBIVV1e6bAad07IwdPuCGmivNvwRuQ== + dependencies: + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-signing@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-signing/-/middleware-signing-3.78.0.tgz#2fb41819a9ae0953cf8f428851a57696442469ca" + integrity sha512-OEjJJCNhHHSOprLZ9CzjHIXEKFtPHWP/bG9pMhkV3/6Bmscsgcf8gWHcOnmIrjqX+hT1VALDNpl/RIh0J6/eQw== + dependencies: + "@aws-sdk/property-provider" "3.78.0" + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/signature-v4" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-ssec@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-ssec/-/middleware-ssec-3.78.0.tgz#4463c6c6ee26c8b3f2ebc112f7de3ca560ba4f3f" + integrity sha512-3z+UOd95rxvj+iO6WxMjuRNNUMlO6xhXZdBHvQmoiyS+9nMDcNieTu6gfQyLAilVeCh8xU9a0IenJuIYVdJ96g== + dependencies: + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-stack@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-stack/-/middleware-stack-3.78.0.tgz#e9f42039e500bed23ec74359924ae16e7bf9c77a" + integrity sha512-UoNfRh6eAJN3BJHlG1eb+KeuSe+zARTC2cglroJRyHc2j7GxH2i9FD3IJbj5wvzopJEnQzuY/VCs6STFkqWL1g== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/middleware-user-agent@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.78.0.tgz#e4c7345d26d718de0e84b60ba02b2b08b566fa15" + integrity sha512-wdN5uoq8RxxhLhj0EPeuDSRFuXfUwKeEqRzCKMsYAOC0cAm+PryaP2leo0oTGJ9LUK8REK7zyfFcmtC4oOzlkA== + dependencies: + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/node-config-provider@3.80.0": + version "3.80.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/node-config-provider/-/node-config-provider-3.80.0.tgz#dbb02aa48fb1a0acc3201ca73db5bbf1738895b5" + integrity sha512-vyTOMK04huB7n10ZUv0thd2TE6KlY8livOuLqFTMtj99AJ6vyeB5XBNwKnQtJIt/P7CijYgp8KcFvI9fndOmKg== + dependencies: + "@aws-sdk/property-provider" "3.78.0" + "@aws-sdk/shared-ini-file-loader" "3.80.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/node-http-handler@3.82.0": + version "3.82.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/node-http-handler/-/node-http-handler-3.82.0.tgz#e28064815c6c6caf22a16bb7fee4e9e7e73ef3bb" + integrity sha512-yyq/DA/IMzL4fLJhV7zVfP7aUQWPHfOKTCJjWB3KeV5YPiviJtSKb/KyzNi+gQyO7SmsL/8vQbQrf3/s7N/2OA== + dependencies: + "@aws-sdk/abort-controller" "3.78.0" + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/querystring-builder" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/property-provider@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/property-provider/-/property-provider-3.78.0.tgz#f12341fa87da2b54daac95f623bf7ede1754f8ae" + integrity sha512-PZpLvV0hF6lqg3CSN9YmphrB/t5LVJVWGJLB9d9qm7sJs5ksjTYBb5bY91OQ3zit0F4cqBMU8xt2GQ9J6d4DvQ== + dependencies: + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/protocol-http@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/protocol-http/-/protocol-http-3.78.0.tgz#8a30db90e3373fe94e2b0007c3cba47b5c9e08bd" + integrity sha512-SQB26MhEK96yDxyXd3UAaxLz1Y/ZvgE4pzv7V3wZiokdEedM0kawHKEn1UQJlqJLEZcQI9QYyysh3rTvHZ3fyg== + dependencies: + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/querystring-builder@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/querystring-builder/-/querystring-builder-3.78.0.tgz#29068c4d1fad056e26f848779a31335469cb0038" + integrity sha512-aib6RW1WAaTQDqVgRU1Ku9idkhm90gJKbCxVaGId+as6QHNUqMChEfK2v+0afuKiPNOs5uWmqvOXI9+Gt+UGDg== + dependencies: + "@aws-sdk/types" "3.78.0" + "@aws-sdk/util-uri-escape" "3.55.0" + tslib "^2.3.1" + +"@aws-sdk/querystring-parser@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/querystring-parser/-/querystring-parser-3.78.0.tgz#4c76fe15ef2e9bbf4c387c83889d1c25d2c3a614" + integrity sha512-csaH8YTyN+KMNczeK6fBS8l7iJaqcQcKOIbpQFg5upX4Ly5A56HJn4sVQhY1LSgfSk4xRsNfMy5mu6BlsIiaXA== + dependencies: + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/service-error-classification@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/service-error-classification/-/service-error-classification-3.78.0.tgz#8d3ac1064e39c180d9b764bb838c7f9de5615281" + integrity sha512-x7Lx8KWctJa01q4Q72Zb4ol9L/era3vy2daASu8l2paHHxsAPBE0PThkvLdUSLZSzlHSVdh3YHESIsT++VsK4w== + +"@aws-sdk/shared-ini-file-loader@3.80.0": + version "3.80.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/shared-ini-file-loader/-/shared-ini-file-loader-3.80.0.tgz#e3d1b0532e9a884e52f967717ba2666ca32bbd74" + integrity sha512-3d5EBJjnWWkjLK9skqLLHYbagtFaZZy+3jUTlbTuOKhlOwe8jF7CUM3j6I4JA6yXNcB3w0exDKKHa8w+l+05aA== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/signature-v4-multi-region@3.88.0": + version "3.88.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.88.0.tgz#7102caacf3d4722dd1fa4dedbe05bef21064edc5" + integrity sha512-RBbyQRpohlIQiuZc5qAvwbXO0Bob9XhHFS/kuLh+DcyeaBp+m+Bt291FX1Ksz2A0Q3ETNM34LFt7kTOBtMvjIQ== + dependencies: + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/signature-v4" "3.78.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/util-arn-parser" "3.55.0" + tslib "^2.3.1" + +"@aws-sdk/signature-v4@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4/-/signature-v4-3.78.0.tgz#adb735b9604d4bb8e44d16f1baa87618d576013b" + integrity sha512-eePjRYuzKoi3VMr/lgrUEF1ytLeH4fA/NMCykr/uR6NMo4bSJA59KrFLYSM7SlWLRIyB0UvJqygVEvSxFluyDw== + dependencies: + "@aws-sdk/is-array-buffer" "3.55.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/util-hex-encoding" "3.58.0" + "@aws-sdk/util-middleware" "3.78.0" + "@aws-sdk/util-uri-escape" "3.55.0" + tslib "^2.3.1" + +"@aws-sdk/smithy-client@3.85.0": + version "3.85.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/smithy-client/-/smithy-client-3.85.0.tgz#70852daa14fef9af1adfb4411237026cb68943da" + integrity sha512-Ox/yQEAnANzhpJMyrpuxWtF/i3EviavENczT7fo4uwSyZTz/sfSBQNjs/YAG1UeA6uOI3pBP5EaFERV5hr2fRA== + dependencies: + "@aws-sdk/middleware-stack" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/types@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.78.0.tgz#51dc80b2142ee20821fb9f476bdca6e541021443" + integrity sha512-I9PTlVNSbwhIgMfmDM5as1tqRIkVZunjVmfogb2WVVPp4CaX0Ll01S0FSMSLL9k6tcQLXqh45pFRjrxCl9WKdQ== + +"@aws-sdk/types@^3.1.0": + version "3.55.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.55.0.tgz#d524d567e2b2722f2d6be83e2417dd6d46ce1490" + integrity sha512-wrDZjuy1CVAYxDCbm3bWQIKMGfNs7XXmG0eG4858Ixgqmq2avsIn5TORy8ynBxcXn9aekV/+tGEQ7BBSYzIVNQ== + +"@aws-sdk/url-parser@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/url-parser/-/url-parser-3.78.0.tgz#8903011fda4b04c1207df099a21eda1304573099" + integrity sha512-iQn2AjECUoJE0Ae9XtgHtGGKvUkvE8hhbktGopdj+zsPBe4WrBN2DgVxlKPPrBonG/YlcL1D7a5EXaujWSlUUw== + dependencies: + "@aws-sdk/querystring-parser" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/util-arn-parser@3.55.0": + version "3.55.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-arn-parser/-/util-arn-parser-3.55.0.tgz#6672eb2975e798a460bedfaf6b5618d4e4b262e1" + integrity sha512-76KJxp4MRWufHYWys7DFl64znr5yeJ3AIQNAPCKKw1sP0hzO7p6Kx0PaJnw9x+CPSzOrT4NbuApL6/srYhKDGg== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/util-base64-browser@3.58.0": + version "3.58.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-base64-browser/-/util-base64-browser-3.58.0.tgz#e213f91a5d40dd2d048d340f1ab192ca86c1f40c" + integrity sha512-0ebsXIZNpu/fup9OgsFPnRKfCFbuuI9PPRzvP6twzLxUB0c/aix6Co7LGHFKcRKHZdaykoJMXArf8eHj2Nzv1Q== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/util-base64-node@3.55.0": + version "3.55.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-base64-node/-/util-base64-node-3.55.0.tgz#da9a3fd6752be49163572144793e6b23d0186ff4" + integrity sha512-UQ/ZuNoAc8CFMpSiRYmevaTsuRKzLwulZTnM8LNlIt9Wx1tpNvqp80cfvVj7yySKROtEi20wq29h31dZf1eYNQ== + dependencies: + "@aws-sdk/util-buffer-from" "3.55.0" + tslib "^2.3.1" + +"@aws-sdk/util-body-length-browser@3.55.0": + version "3.55.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-body-length-browser/-/util-body-length-browser-3.55.0.tgz#9c2637097501032f6a1afddb76687415fe9b44b6" + integrity sha512-Ei2OCzXQw5N6ZkTMZbamUzc1z+z1R1Ja5tMEagz5BxuX4vWdBObT+uGlSzL8yvTbjoPjnxWA2aXyEqaUP3JS8Q== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/util-body-length-node@3.55.0": + version "3.55.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-body-length-node/-/util-body-length-node-3.55.0.tgz#67049bbb6c62d794a1bb5a13b9a678988c925489" + integrity sha512-lU1d4I+9wJwydduXs0SxSfd+mHKjxeyd39VwOv6i2KSwWkPbji9UQqpflKLKw+r45jL7+xU/zfeTUg5Tt/3Gew== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/util-buffer-from@3.55.0": + version "3.55.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-buffer-from/-/util-buffer-from-3.55.0.tgz#e7c927974b07a29502aa1ad58509b91d0d7cf0f7" + integrity sha512-uVzKG1UgvnV7XX2FPTylBujYMKBPBaq/qFBxfl0LVNfrty7YjpfieQxAe6yRLD+T0Kir/WDQwGvYC+tOYG3IGA== + dependencies: + "@aws-sdk/is-array-buffer" "3.55.0" + tslib "^2.3.1" + +"@aws-sdk/util-config-provider@3.55.0": + version "3.55.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-config-provider/-/util-config-provider-3.55.0.tgz#720c6c0ac1aa8d14be29d1dee25e01eb4925c0ce" + integrity sha512-30dzofQQfx6tp1jVZkZ0DGRsT0wwC15nEysKRiAcjncM64A0Cm6sra77d0os3vbKiKoPCI/lMsFr4o3533+qvQ== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/util-defaults-mode-browser@3.85.0": + version "3.85.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-defaults-mode-browser/-/util-defaults-mode-browser-3.85.0.tgz#215e99e8815f885ce722668a0e5afbbca69fa964" + integrity sha512-oqK/e2pHuMWrvTJWtDBzylbj232ezlTay5dCq4RQlyi3LPPVBQ08haYD1Mk2ikQ/qa0XvbSD6YVhjpTlvwRNjw== + dependencies: + "@aws-sdk/property-provider" "3.78.0" + "@aws-sdk/types" "3.78.0" + bowser "^2.11.0" + tslib "^2.3.1" + +"@aws-sdk/util-defaults-mode-node@3.85.0": + version "3.85.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-defaults-mode-node/-/util-defaults-mode-node-3.85.0.tgz#8cd27ea50ddce298ec586d36eb6379ba14d7bfaf" + integrity sha512-KDNl4H8jJJLh6y7I3MSwRKe4plKbFKK8MVkS0+Fce/GJh4EnqxF0HzMMaSeNUcPvO2wHRq2a60+XW+0d7eWo1A== + dependencies: + "@aws-sdk/config-resolver" "3.80.0" + "@aws-sdk/credential-provider-imds" "3.81.0" + "@aws-sdk/node-config-provider" "3.80.0" + "@aws-sdk/property-provider" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/util-dynamodb@3.87.0": + version "3.87.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-dynamodb/-/util-dynamodb-3.87.0.tgz#d1f2e05ecbefd762cb31315ad61228f56e7cd87c" + integrity sha512-Qh6bHZdVd8+y2h2eJmfYPa5BwNaY65Q135ZbHypQCjiR87Xy50qdVlfWvVmn0EedLqvDVZMPpBrrrcPXMaqvAg== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/util-dynamodb@3.92.0": + version "3.92.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-dynamodb/-/util-dynamodb-3.92.0.tgz#3358f4061a4b0dfea749399c9facf468909efa7b" + integrity sha512-MsrcL5UoANjtgUN5Gp4OE0r4s4QbLZ/5CnQVsngC+WHb1cT+AHP0Mfot+UKClL5BxfIrbtkjFi8YucIofzKp1w== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/util-hex-encoding@3.58.0": + version "3.58.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-hex-encoding/-/util-hex-encoding-3.58.0.tgz#d999eb19329933a94563881540a06d7ac7f515f5" + integrity sha512-Rl+jXUzk/FJkOLYfUVYPhKa2aUmTpeobRP31l8IatQltSzDgLyRHO35f6UEs7Ztn5s1jbu/POatLAZ2WjbgVyg== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/util-locate-window@^3.0.0": + version "3.55.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-locate-window/-/util-locate-window-3.55.0.tgz#a4136a20ee1bfcb73967a6614caf769ef79db070" + integrity sha512-0sPmK2JaJE2BbTcnvybzob/VrFKCXKfN4CUKcvn0yGg/me7Bz+vtzQRB3Xp+YSx+7OtWxzv63wsvHoAnXvgxgg== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/util-middleware@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-middleware/-/util-middleware-3.78.0.tgz#d907a9b8b7878265cd3e3ee15996bc17de41db11" + integrity sha512-Hi3wv2b0VogO4mzyeEaeU5KgIt4qeo0LXU5gS6oRrG0T7s2FyKbMBkJW3YDh/Y8fNwqArZ+/QQFujpP0PIKwkA== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/util-stream-browser@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-stream-browser/-/util-stream-browser-3.78.0.tgz#d4578ab9d1ff882f792f3381604c90718310405c" + integrity sha512-EcThf/sJoD4NYTUNO/nehR57lqkOuL6btRoVnm4LGUR8XgQcJ/WMYYgxOMY8E81xXzRFX2ukRHRxL2xmQsbHDw== + dependencies: + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/util-stream-node@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-stream-node/-/util-stream-node-3.78.0.tgz#37b2f07e4ec3b325d93bd49c7eedf5d891c8d69b" + integrity sha512-CHfX37ioUyamAnlS2p4Nq+4BBjCSlZolFkVyxtVJwzPBBksdvjW67nKG+SShR48RBPJ5LEzbgAaEXNRktCSf6w== + dependencies: + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/util-uri-escape@3.55.0": + version "3.55.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-uri-escape/-/util-uri-escape-3.55.0.tgz#ee57743c628a1c9f942dfe73205ce890ec011916" + integrity sha512-mmdDLUpFCN2nkfwlLdOM54lTD528GiGSPN1qb8XtGLgZsJUmg3uJSFIN2lPeSbEwJB3NFjVas/rnQC48i7mV8w== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/util-user-agent-browser@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.78.0.tgz#12509ed9cc77624da0e0c017099565e37a5038d0" + integrity sha512-diGO/Bf4ggBOEnfD7lrrXaaXOwOXGz0bAJ0HhpizwEMlBld5zfDlWXjNpslh+8+u3EHRjPJQ16KGT6mp/Dm+aw== + dependencies: + "@aws-sdk/types" "3.78.0" + bowser "^2.11.0" + tslib "^2.3.1" + +"@aws-sdk/util-user-agent-node@3.80.0": + version "3.80.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.80.0.tgz#269ea0f9bfab4f378af759afa9137936081f010a" + integrity sha512-QV26qIXws1m6sZXg65NS+XrQ5NhAzbDVQLtEVE4nC39UN8fuieP6Uet/gZm9mlLI9hllwvcV7EfgBM3GSC7pZg== + dependencies: + "@aws-sdk/node-config-provider" "3.80.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/util-utf8-browser@3.55.0", "@aws-sdk/util-utf8-browser@^3.0.0": + version "3.55.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.55.0.tgz#a045bf1a93f6e0ff9c846631b168ea55bbb37668" + integrity sha512-ljzqJcyjfJpEVSIAxwtIS8xMRUly84BdjlBXyp6cu4G8TUufgjNS31LWdhyGhgmW5vYBNr+LTz0Kwf6J+ou7Ug== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/util-utf8-node@3.55.0": + version "3.55.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-utf8-node/-/util-utf8-node-3.55.0.tgz#44cf9f9c8624d144afd65ab8a1786e33134add15" + integrity sha512-FsFm7GFaC7j0tlPEm/ri8bU2QCwFW5WKjxUg8lm1oWaxplCpKGUsmcfPJ4sw58GIoyoGu4QXBK60oCWosZYYdQ== + dependencies: + "@aws-sdk/util-buffer-from" "3.55.0" + tslib "^2.3.1" + +"@aws-sdk/util-waiter@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-waiter/-/util-waiter-3.78.0.tgz#5886f3e06ae6df9a12ef7079a6e75c76921ea4da" + integrity sha512-8pWd0XiNOS8AkWQyac8VNEI+gz/cGWlC2TAE2CJp0rOK5XhvlcNBINai4D6TxQ+9foyJXLOI1b8nuXemekoG8A== + dependencies: + "@aws-sdk/abort-controller" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/xml-builder@3.55.0": + version "3.55.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/xml-builder/-/xml-builder-3.55.0.tgz#8e14012ab3ed27cf68964abf1326d06c686b3511" + integrity sha512-BH+i5S2FLprmfSeIuGy3UbNtEoJPVjh8arl5+LV3i2KY/+TmrS4yT8JtztDlDxHF0cMtNLZNO0KEPtsACS6SOg== + dependencies: + tslib "^2.3.1" + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.7.tgz#44416b6bd7624b998f5b1af5d470856c40138789" + integrity sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg== + dependencies: + "@babel/highlight" "^7.16.7" + +"@babel/compat-data@^7.17.7": + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.17.7.tgz#078d8b833fbbcc95286613be8c716cef2b519fa2" + integrity sha512-p8pdE6j0a29TNGebNm7NzYZWB3xVZJBZ7XGs42uAKzQo8VQ3F0By/cQCtUEABwIqw5zo6WA4NbmxsfzADzMKnQ== + +"@babel/core@^7.1.0", "@babel/core@^7.12.3", "@babel/core@^7.7.2", "@babel/core@^7.8.0": + version "7.17.8" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.17.8.tgz#3dac27c190ebc3a4381110d46c80e77efe172e1a" + integrity sha512-OdQDV/7cRBtJHLSOBqqbYNkOcydOgnX59TZx4puf41fzcVtN3e/4yqY8lMQsK+5X2lJtAdmA+6OHqsj1hBJ4IQ== + dependencies: + "@ampproject/remapping" "^2.1.0" + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.17.7" + "@babel/helper-compilation-targets" "^7.17.7" + "@babel/helper-module-transforms" "^7.17.7" + "@babel/helpers" "^7.17.8" + "@babel/parser" "^7.17.8" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.17.3" + "@babel/types" "^7.17.0" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.1.2" + semver "^6.3.0" + +"@babel/generator@^7.17.3", "@babel/generator@^7.17.7", "@babel/generator@^7.7.2": + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.17.7.tgz#8da2599beb4a86194a3b24df6c085931d9ee45ad" + integrity sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w== + dependencies: + "@babel/types" "^7.17.0" + jsesc "^2.5.1" + source-map "^0.5.0" + +"@babel/helper-compilation-targets@^7.17.7": + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.7.tgz#a3c2924f5e5f0379b356d4cfb313d1414dc30e46" + integrity sha512-UFzlz2jjd8kroj0hmCFV5zr+tQPi1dpC2cRsDV/3IEW8bJfCPrPpmcSN6ZS8RqIq4LXcmpipCQFPddyFA5Yc7w== + dependencies: + "@babel/compat-data" "^7.17.7" + "@babel/helper-validator-option" "^7.16.7" + browserslist "^4.17.5" + semver "^6.3.0" + +"@babel/helper-environment-visitor@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz#ff484094a839bde9d89cd63cba017d7aae80ecd7" + integrity sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-function-name@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz#f1ec51551fb1c8956bc8dd95f38523b6cf375f8f" + integrity sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA== + dependencies: + "@babel/helper-get-function-arity" "^7.16.7" + "@babel/template" "^7.16.7" + "@babel/types" "^7.16.7" + +"@babel/helper-get-function-arity@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz#ea08ac753117a669f1508ba06ebcc49156387419" + integrity sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-hoist-variables@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz#86bcb19a77a509c7b77d0e22323ef588fa58c246" + integrity sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-module-imports@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz#25612a8091a999704461c8a222d0efec5d091437" + integrity sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-module-transforms@^7.17.7": + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz#3943c7f777139e7954a5355c815263741a9c1cbd" + integrity sha512-VmZD99F3gNTYB7fJRDTi+u6l/zxY0BE6OIxPSU7a50s6ZUQkHwSDmV92FfM+oCG0pZRVojGYhkR8I0OGeCVREw== + dependencies: + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-module-imports" "^7.16.7" + "@babel/helper-simple-access" "^7.17.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/helper-validator-identifier" "^7.16.7" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.17.3" + "@babel/types" "^7.17.0" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.8.0": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz#aa3a8ab4c3cceff8e65eb9e73d87dc4ff320b2f5" + integrity sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA== + +"@babel/helper-simple-access@^7.17.7": + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.17.7.tgz#aaa473de92b7987c6dfa7ce9a7d9674724823367" + integrity sha512-txyMCGroZ96i+Pxr3Je3lzEJjqwaRC9buMUgtomcrLe5Nd0+fk1h0LLA+ixUF5OW7AhHuQ7Es1WcQJZmZsz2XA== + dependencies: + "@babel/types" "^7.17.0" + +"@babel/helper-split-export-declaration@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz#0b648c0c42da9d3920d85ad585f2778620b8726b" + integrity sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-validator-identifier@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad" + integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw== + +"@babel/helper-validator-option@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz#b203ce62ce5fe153899b617c08957de860de4d23" + integrity sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ== + +"@babel/helpers@^7.17.8": + version "7.17.8" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.17.8.tgz#288450be8c6ac7e4e44df37bcc53d345e07bc106" + integrity sha512-QcL86FGxpfSJwGtAvv4iG93UL6bmqBdmoVY0CMCU2g+oD2ezQse3PT5Pa+jiD6LJndBQi0EDlpzOWNlLuhz5gw== + dependencies: + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.17.3" + "@babel/types" "^7.17.0" + +"@babel/highlight@^7.16.7": + version "7.16.10" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.16.10.tgz#744f2eb81579d6eea753c227b0f570ad785aba88" + integrity sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw== + dependencies: + "@babel/helper-validator-identifier" "^7.16.7" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.16.7", "@babel/parser@^7.17.3", "@babel/parser@^7.17.8": + version "7.17.8" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.8.tgz#2817fb9d885dd8132ea0f8eb615a6388cca1c240" + integrity sha512-BoHhDJrJXqcg+ZL16Xv39H9n+AqJ4pcDrQBGZN+wHxIysrLZ3/ECwCBUch/1zUNhnsXULcONU3Ei5Hmkfk6kiQ== + +"@babel/plugin-syntax-async-generators@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-bigint@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea" + integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.8.3": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" + integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== + dependencies: + "@babel/helper-plugin-utils" "^7.12.13" + +"@babel/plugin-syntax-import-meta@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" + integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-json-strings@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-logical-assignment-operators@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-object-rest-spread@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-top-level-await@^7.8.3": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-typescript@^7.7.2": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.16.7.tgz#39c9b55ee153151990fb038651d58d3fd03f98f8" + integrity sha512-YhUIJHHGkqPgEcMYkPCKTyGUdoGKWtopIycQyjJH8OjvRgOYsXsaKehLVPScKJWAULPxMa4N1vCe6szREFlZ7A== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/template@^7.16.7", "@babel/template@^7.3.3": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155" + integrity sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/parser" "^7.16.7" + "@babel/types" "^7.16.7" + +"@babel/traverse@^7.17.3", "@babel/traverse@^7.7.2": + version "7.17.3" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.17.3.tgz#0ae0f15b27d9a92ba1f2263358ea7c4e7db47b57" + integrity sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.17.3" + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-function-name" "^7.16.7" + "@babel/helper-hoist-variables" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/parser" "^7.17.3" + "@babel/types" "^7.17.0" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/types@^7.0.0", "@babel/types@^7.16.7", "@babel/types@^7.17.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3": + version "7.17.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.17.0.tgz#a826e368bccb6b3d84acd76acad5c0d87342390b" + integrity sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw== + dependencies: + "@babel/helper-validator-identifier" "^7.16.7" + to-fast-properties "^2.0.0" + +"@balena/dockerignore@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@balena/dockerignore/-/dockerignore-1.0.2.tgz#9ffe4726915251e8eb69f44ef3547e0da2c03e0d" + integrity sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q== + +"@bcoe/v8-coverage@^0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" + integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== + +"@colors/colors@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" + integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== + +"@cspotcode/source-map-consumer@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b" + integrity sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg== + +"@cspotcode/source-map-support@0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz#4789840aa859e46d2f3173727ab707c66bf344f5" + integrity sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA== + dependencies: + "@cspotcode/source-map-consumer" "0.8.0" + +"@dabh/diagnostics@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.3.tgz#7f7e97ee9a725dffc7808d93668cc984e1dc477a" + integrity sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA== + dependencies: + colorspace "1.1.x" + enabled "2.0.x" + kuler "^2.0.0" + +"@eslint/eslintrc@^1.0.5": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.2.1.tgz#8b5e1c49f4077235516bc9ec7d41378c0f69b8c6" + integrity sha512-bxvbYnBPN1Gibwyp6NrpnFzA3YtRL3BBAyEAFVIpNTm2Rn4Vy87GA5M4aSn3InRrlsbX5N0GW7XIx+U4SAEKdQ== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.3.1" + globals "^13.9.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.0.4" + strip-json-comments "^3.1.1" + +"@gar/promisify@^1.0.1": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" + integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== + +"@humanwhocodes/config-array@^0.9.2": + version "0.9.5" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.9.5.tgz#2cbaf9a89460da24b5ca6531b8bbfc23e1df50c7" + integrity sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw== + dependencies: + "@humanwhocodes/object-schema" "^1.2.1" + debug "^4.1.1" + minimatch "^3.0.4" + +"@humanwhocodes/object-schema@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" + integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== + +"@hutson/parse-repository-url@^3.0.0": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz#98c23c950a3d9b6c8f0daed06da6c3af06981340" + integrity sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q== + +"@istanbuljs/load-nyc-config@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" + integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== + dependencies: + camelcase "^5.3.1" + find-up "^4.1.0" + get-package-type "^0.1.0" + js-yaml "^3.13.1" + resolve-from "^5.0.0" + +"@istanbuljs/schema@^0.1.2": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" + integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== + +"@jest/console@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-27.5.1.tgz#260fe7239602fe5130a94f1aa386eff54b014bba" + integrity sha512-kZ/tNpS3NXn0mlXXXPNuDZnb4c0oZ20r4K5eemM2k30ZC3G0T02nXUvyhf5YdbXWHPEJLc9qGLxEZ216MdL+Zg== + dependencies: + "@jest/types" "^27.5.1" + "@types/node" "*" + chalk "^4.0.0" + jest-message-util "^27.5.1" + jest-util "^27.5.1" + slash "^3.0.0" + +"@jest/core@^27.4.3", "@jest/core@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-27.5.1.tgz#267ac5f704e09dc52de2922cbf3af9edcd64b626" + integrity sha512-AK6/UTrvQD0Cd24NSqmIA6rKsu0tKIxfiCducZvqxYdmMisOYAsdItspT+fQDQYARPf8XgjAFZi0ogW2agH5nQ== + dependencies: + "@jest/console" "^27.5.1" + "@jest/reporters" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + emittery "^0.8.1" + exit "^0.1.2" + graceful-fs "^4.2.9" + jest-changed-files "^27.5.1" + jest-config "^27.5.1" + jest-haste-map "^27.5.1" + jest-message-util "^27.5.1" + jest-regex-util "^27.5.1" + jest-resolve "^27.5.1" + jest-resolve-dependencies "^27.5.1" + jest-runner "^27.5.1" + jest-runtime "^27.5.1" + jest-snapshot "^27.5.1" + jest-util "^27.5.1" + jest-validate "^27.5.1" + jest-watcher "^27.5.1" + micromatch "^4.0.4" + rimraf "^3.0.0" + slash "^3.0.0" + strip-ansi "^6.0.0" + +"@jest/environment@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-27.5.1.tgz#d7425820511fe7158abbecc010140c3fd3be9c74" + integrity sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA== + dependencies: + "@jest/fake-timers" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/node" "*" + jest-mock "^27.5.1" + +"@jest/fake-timers@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-27.5.1.tgz#76979745ce0579c8a94a4678af7a748eda8ada74" + integrity sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ== + dependencies: + "@jest/types" "^27.5.1" + "@sinonjs/fake-timers" "^8.0.1" + "@types/node" "*" + jest-message-util "^27.5.1" + jest-mock "^27.5.1" + jest-util "^27.5.1" + +"@jest/globals@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-27.5.1.tgz#7ac06ce57ab966566c7963431cef458434601b2b" + integrity sha512-ZEJNB41OBQQgGzgyInAv0UUfDDj3upmHydjieSxFvTRuZElrx7tXg/uVQ5hYVEwiXs3+aMsAeEc9X7xiSKCm4Q== + dependencies: + "@jest/environment" "^27.5.1" + "@jest/types" "^27.5.1" + expect "^27.5.1" + +"@jest/reporters@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-27.5.1.tgz#ceda7be96170b03c923c37987b64015812ffec04" + integrity sha512-cPXh9hWIlVJMQkVk84aIvXuBB4uQQmFqZiacloFuGiP3ah1sbCxCosidXFDfqG8+6fO1oR2dTJTlsOy4VFmUfw== + dependencies: + "@bcoe/v8-coverage" "^0.2.3" + "@jest/console" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/node" "*" + chalk "^4.0.0" + collect-v8-coverage "^1.0.0" + exit "^0.1.2" + glob "^7.1.2" + graceful-fs "^4.2.9" + istanbul-lib-coverage "^3.0.0" + istanbul-lib-instrument "^5.1.0" + istanbul-lib-report "^3.0.0" + istanbul-lib-source-maps "^4.0.0" + istanbul-reports "^3.1.3" + jest-haste-map "^27.5.1" + jest-resolve "^27.5.1" + jest-util "^27.5.1" + jest-worker "^27.5.1" + slash "^3.0.0" + source-map "^0.6.0" + string-length "^4.0.1" + terminal-link "^2.0.0" + v8-to-istanbul "^8.1.0" + +"@jest/source-map@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-27.5.1.tgz#6608391e465add4205eae073b55e7f279e04e8cf" + integrity sha512-y9NIHUYF3PJRlHk98NdC/N1gl88BL08aQQgu4k4ZopQkCw9t9cV8mtl3TV8b/YCB8XaVTFrmUTAJvjsntDireg== + dependencies: + callsites "^3.0.0" + graceful-fs "^4.2.9" + source-map "^0.6.0" + +"@jest/test-result@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-27.5.1.tgz#56a6585fa80f7cdab72b8c5fc2e871d03832f5bb" + integrity sha512-EW35l2RYFUcUQxFJz5Cv5MTOxlJIQs4I7gxzi2zVU7PJhOwfYq1MdC5nhSmYjX1gmMmLPvB3sIaC+BkcHRBfag== + dependencies: + "@jest/console" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/istanbul-lib-coverage" "^2.0.0" + collect-v8-coverage "^1.0.0" + +"@jest/test-sequencer@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-27.5.1.tgz#4057e0e9cea4439e544c6353c6affe58d095745b" + integrity sha512-LCheJF7WB2+9JuCS7VB/EmGIdQuhtqjRNI9A43idHv3E4KltCTsPsLxvdaubFHSYwY/fNjMWjl6vNRhDiN7vpQ== + dependencies: + "@jest/test-result" "^27.5.1" + graceful-fs "^4.2.9" + jest-haste-map "^27.5.1" + jest-runtime "^27.5.1" + +"@jest/transform@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-27.5.1.tgz#6c3501dcc00c4c08915f292a600ece5ecfe1f409" + integrity sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw== + dependencies: + "@babel/core" "^7.1.0" + "@jest/types" "^27.5.1" + babel-plugin-istanbul "^6.1.1" + chalk "^4.0.0" + convert-source-map "^1.4.0" + fast-json-stable-stringify "^2.0.0" + graceful-fs "^4.2.9" + jest-haste-map "^27.5.1" + jest-regex-util "^27.5.1" + jest-util "^27.5.1" + micromatch "^4.0.4" + pirates "^4.0.4" + slash "^3.0.0" + source-map "^0.6.1" + write-file-atomic "^3.0.0" + +"@jest/types@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.5.1.tgz#3c79ec4a8ba61c170bf937bcf9e98a9df175ec80" + integrity sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^16.0.0" + chalk "^4.0.0" + +"@jridgewell/resolve-uri@^3.0.3": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz#68eb521368db76d040a6315cdb24bf2483037b9c" + integrity sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.11" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz#771a1d8d744eeb71b6adb35808e1a6c7b9b8c8ec" + integrity sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg== + +"@jridgewell/trace-mapping@^0.3.0": + version "0.3.4" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.4.tgz#f6a0832dffd5b8a6aaa633b7d9f8e8e94c83a0c3" + integrity sha512-vFv9ttIedivx0ux3QSjhgtCVjPZd5l46ZOMDSCwnH1yUO2e964gO8LZGyv2QkqcgR6TnBU1v+1IFqmeoG+0UJQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@jsii/check-node@1.47.0": + version "1.47.0" + resolved "https://registry.yarnpkg.com/@jsii/check-node/-/check-node-1.47.0.tgz#746a548b9de6b4fced4d57d6fa0384943e6a9576" + integrity sha512-LSlbKTpMVYw1R3Be70sJJdJbuLWEFAMbGEHE731Je1QDTXTRm6Gc3NDvPUvTTuHEry8f2Wys+1pXNX06X4PKxQ== + dependencies: + chalk "^4.1.2" + semver "^7.3.5" + +"@jsii/check-node@1.55.1": + version "1.55.1" + resolved "https://registry.yarnpkg.com/@jsii/check-node/-/check-node-1.55.1.tgz#f034cf63cfc31bdaee477008a96443faaf9f5895" + integrity sha512-JC9b+y4CXdIICDE6fYjaN0VKPc65lz4dj1T4lnaqMfbXIBGB7sBMLQJ/szVc9U3Z+o/HBzCJndNn86kGRM+lqw== + dependencies: + chalk "^4.1.2" + semver "^7.3.5" + +"@jsii/spec@1.55.1", "@jsii/spec@^1.47.0", "@jsii/spec@^1.55.1": + version "1.55.1" + resolved "https://registry.yarnpkg.com/@jsii/spec/-/spec-1.55.1.tgz#fb8b820a6aa6c809773207e1f6be65f92ff38296" + integrity sha512-KSKKN04eO0rTaqzw6j9RTx8HAzhePdmWRC3iJQ90QeZLv/L8Pj4l+nZ4wn77BGxmeULpXkGXUKbhkceArdr4GA== + dependencies: + jsonschema "^1.4.0" + +"@lerna/add@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/add/-/add-4.0.0.tgz#c36f57d132502a57b9e7058d1548b7a565ef183f" + integrity sha512-cpmAH1iS3k8JBxNvnMqrGTTjbY/ZAiKa1ChJzFevMYY3eeqbvhsBKnBcxjRXtdrJ6bd3dCQM+ZtK+0i682Fhng== + dependencies: + "@lerna/bootstrap" "4.0.0" + "@lerna/command" "4.0.0" + "@lerna/filter-options" "4.0.0" + "@lerna/npm-conf" "4.0.0" + "@lerna/validation-error" "4.0.0" + dedent "^0.7.0" + npm-package-arg "^8.1.0" + p-map "^4.0.0" + pacote "^11.2.6" + semver "^7.3.4" + +"@lerna/bootstrap@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/bootstrap/-/bootstrap-4.0.0.tgz#5f5c5e2c6cfc8fcec50cb2fbe569a8c607101891" + integrity sha512-RkS7UbeM2vu+kJnHzxNRCLvoOP9yGNgkzRdy4UV2hNalD7EP41bLvRVOwRYQ7fhc2QcbhnKNdOBihYRL0LcKtw== + dependencies: + "@lerna/command" "4.0.0" + "@lerna/filter-options" "4.0.0" + "@lerna/has-npm-version" "4.0.0" + "@lerna/npm-install" "4.0.0" + "@lerna/package-graph" "4.0.0" + "@lerna/pulse-till-done" "4.0.0" + "@lerna/rimraf-dir" "4.0.0" + "@lerna/run-lifecycle" "4.0.0" + "@lerna/run-topologically" "4.0.0" + "@lerna/symlink-binary" "4.0.0" + "@lerna/symlink-dependencies" "4.0.0" + "@lerna/validation-error" "4.0.0" + dedent "^0.7.0" + get-port "^5.1.1" + multimatch "^5.0.0" + npm-package-arg "^8.1.0" + npmlog "^4.1.2" + p-map "^4.0.0" + p-map-series "^2.1.0" + p-waterfall "^2.1.1" + read-package-tree "^5.3.1" + semver "^7.3.4" + +"@lerna/changed@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/changed/-/changed-4.0.0.tgz#b9fc76cea39b9292a6cd263f03eb57af85c9270b" + integrity sha512-cD+KuPRp6qiPOD+BO6S6SN5cARspIaWSOqGBpGnYzLb4uWT8Vk4JzKyYtc8ym1DIwyoFXHosXt8+GDAgR8QrgQ== + dependencies: + "@lerna/collect-updates" "4.0.0" + "@lerna/command" "4.0.0" + "@lerna/listable" "4.0.0" + "@lerna/output" "4.0.0" + +"@lerna/check-working-tree@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/check-working-tree/-/check-working-tree-4.0.0.tgz#257e36a602c00142e76082a19358e3e1ae8dbd58" + integrity sha512-/++bxM43jYJCshBiKP5cRlCTwSJdRSxVmcDAXM+1oUewlZJVSVlnks5eO0uLxokVFvLhHlC5kHMc7gbVFPHv6Q== + dependencies: + "@lerna/collect-uncommitted" "4.0.0" + "@lerna/describe-ref" "4.0.0" + "@lerna/validation-error" "4.0.0" + +"@lerna/child-process@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/child-process/-/child-process-4.0.0.tgz#341b96a57dffbd9705646d316e231df6fa4df6e1" + integrity sha512-XtCnmCT9eyVsUUHx6y/CTBYdV9g2Cr/VxyseTWBgfIur92/YKClfEtJTbOh94jRT62hlKLqSvux/UhxXVh613Q== + dependencies: + chalk "^4.1.0" + execa "^5.0.0" + strong-log-transformer "^2.1.0" + +"@lerna/clean@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/clean/-/clean-4.0.0.tgz#8f778b6f2617aa2a936a6b5e085ae62498e57dc5" + integrity sha512-uugG2iN9k45ITx2jtd8nEOoAtca8hNlDCUM0N3lFgU/b1mEQYAPRkqr1qs4FLRl/Y50ZJ41wUz1eazS+d/0osA== + dependencies: + "@lerna/command" "4.0.0" + "@lerna/filter-options" "4.0.0" + "@lerna/prompt" "4.0.0" + "@lerna/pulse-till-done" "4.0.0" + "@lerna/rimraf-dir" "4.0.0" + p-map "^4.0.0" + p-map-series "^2.1.0" + p-waterfall "^2.1.1" + +"@lerna/cli@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/cli/-/cli-4.0.0.tgz#8eabd334558836c1664df23f19acb95e98b5bbf3" + integrity sha512-Neaw3GzFrwZiRZv2g7g6NwFjs3er1vhraIniEs0jjVLPMNC4eata0na3GfE5yibkM/9d3gZdmihhZdZ3EBdvYA== + dependencies: + "@lerna/global-options" "4.0.0" + dedent "^0.7.0" + npmlog "^4.1.2" + yargs "^16.2.0" + +"@lerna/collect-uncommitted@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/collect-uncommitted/-/collect-uncommitted-4.0.0.tgz#855cd64612969371cfc2453b90593053ff1ba779" + integrity sha512-ufSTfHZzbx69YNj7KXQ3o66V4RC76ffOjwLX0q/ab//61bObJ41n03SiQEhSlmpP+gmFbTJ3/7pTe04AHX9m/g== + dependencies: + "@lerna/child-process" "4.0.0" + chalk "^4.1.0" + npmlog "^4.1.2" + +"@lerna/collect-updates@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/collect-updates/-/collect-updates-4.0.0.tgz#8e208b1bafd98a372ff1177f7a5e288f6bea8041" + integrity sha512-bnNGpaj4zuxsEkyaCZLka9s7nMs58uZoxrRIPJ+nrmrZYp1V5rrd+7/NYTuunOhY2ug1sTBvTAxj3NZQ+JKnOw== + dependencies: + "@lerna/child-process" "4.0.0" + "@lerna/describe-ref" "4.0.0" + minimatch "^3.0.4" + npmlog "^4.1.2" + slash "^3.0.0" + +"@lerna/command@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/command/-/command-4.0.0.tgz#991c7971df8f5bf6ae6e42c808869a55361c1b98" + integrity sha512-LM9g3rt5FsPNFqIHUeRwWXLNHJ5NKzOwmVKZ8anSp4e1SPrv2HNc1V02/9QyDDZK/w+5POXH5lxZUI1CHaOK/A== + dependencies: + "@lerna/child-process" "4.0.0" + "@lerna/package-graph" "4.0.0" + "@lerna/project" "4.0.0" + "@lerna/validation-error" "4.0.0" + "@lerna/write-log-file" "4.0.0" + clone-deep "^4.0.1" + dedent "^0.7.0" + execa "^5.0.0" + is-ci "^2.0.0" + npmlog "^4.1.2" + +"@lerna/conventional-commits@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/conventional-commits/-/conventional-commits-4.0.0.tgz#660fb2c7b718cb942ead70110df61f18c6f99750" + integrity sha512-CSUQRjJHFrH8eBn7+wegZLV3OrNc0Y1FehYfYGhjLE2SIfpCL4bmfu/ViYuHh9YjwHaA+4SX6d3hR+xkeseKmw== + dependencies: + "@lerna/validation-error" "4.0.0" + conventional-changelog-angular "^5.0.12" + conventional-changelog-core "^4.2.2" + conventional-recommended-bump "^6.1.0" + fs-extra "^9.1.0" + get-stream "^6.0.0" + lodash.template "^4.5.0" + npm-package-arg "^8.1.0" + npmlog "^4.1.2" + pify "^5.0.0" + semver "^7.3.4" + +"@lerna/create-symlink@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/create-symlink/-/create-symlink-4.0.0.tgz#8c5317ce5ae89f67825443bd7651bf4121786228" + integrity sha512-I0phtKJJdafUiDwm7BBlEUOtogmu8+taxq6PtIrxZbllV9hWg59qkpuIsiFp+no7nfRVuaasNYHwNUhDAVQBig== + dependencies: + cmd-shim "^4.1.0" + fs-extra "^9.1.0" + npmlog "^4.1.2" + +"@lerna/create@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/create/-/create-4.0.0.tgz#b6947e9b5dfb6530321952998948c3e63d64d730" + integrity sha512-mVOB1niKByEUfxlbKTM1UNECWAjwUdiioIbRQZEeEabtjCL69r9rscIsjlGyhGWCfsdAG5wfq4t47nlDXdLLag== + dependencies: + "@lerna/child-process" "4.0.0" + "@lerna/command" "4.0.0" + "@lerna/npm-conf" "4.0.0" + "@lerna/validation-error" "4.0.0" + dedent "^0.7.0" + fs-extra "^9.1.0" + globby "^11.0.2" + init-package-json "^2.0.2" + npm-package-arg "^8.1.0" + p-reduce "^2.1.0" + pacote "^11.2.6" + pify "^5.0.0" + semver "^7.3.4" + slash "^3.0.0" + validate-npm-package-license "^3.0.4" + validate-npm-package-name "^3.0.0" + whatwg-url "^8.4.0" + yargs-parser "20.2.4" + +"@lerna/describe-ref@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/describe-ref/-/describe-ref-4.0.0.tgz#53c53b4ea65fdceffa072a62bfebe6772c45d9ec" + integrity sha512-eTU5+xC4C5Gcgz+Ey4Qiw9nV2B4JJbMulsYJMW8QjGcGh8zudib7Sduj6urgZXUYNyhYpRs+teci9M2J8u+UvQ== + dependencies: + "@lerna/child-process" "4.0.0" + npmlog "^4.1.2" + +"@lerna/diff@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/diff/-/diff-4.0.0.tgz#6d3071817aaa4205a07bf77cfc6e932796d48b92" + integrity sha512-jYPKprQVg41+MUMxx6cwtqsNm0Yxx9GDEwdiPLwcUTFx+/qKCEwifKNJ1oGIPBxyEHX2PFCOjkK39lHoj2qiag== + dependencies: + "@lerna/child-process" "4.0.0" + "@lerna/command" "4.0.0" + "@lerna/validation-error" "4.0.0" + npmlog "^4.1.2" + +"@lerna/exec@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/exec/-/exec-4.0.0.tgz#eb6cb95cb92d42590e9e2d628fcaf4719d4a8be6" + integrity sha512-VGXtL/b/JfY84NB98VWZpIExfhLOzy0ozm/0XaS4a2SmkAJc5CeUfrhvHxxkxiTBLkU+iVQUyYEoAT0ulQ8PCw== + dependencies: + "@lerna/child-process" "4.0.0" + "@lerna/command" "4.0.0" + "@lerna/filter-options" "4.0.0" + "@lerna/profiler" "4.0.0" + "@lerna/run-topologically" "4.0.0" + "@lerna/validation-error" "4.0.0" + p-map "^4.0.0" + +"@lerna/filter-options@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/filter-options/-/filter-options-4.0.0.tgz#ac94cc515d7fa3b47e2f7d74deddeabb1de5e9e6" + integrity sha512-vV2ANOeZhOqM0rzXnYcFFCJ/kBWy/3OA58irXih9AMTAlQLymWAK0akWybl++sUJ4HB9Hx12TOqaXbYS2NM5uw== + dependencies: + "@lerna/collect-updates" "4.0.0" + "@lerna/filter-packages" "4.0.0" + dedent "^0.7.0" + npmlog "^4.1.2" + +"@lerna/filter-packages@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/filter-packages/-/filter-packages-4.0.0.tgz#b1f70d70e1de9cdd36a4e50caa0ac501f8d012f2" + integrity sha512-+4AJIkK7iIiOaqCiVTYJxh/I9qikk4XjNQLhE3kixaqgMuHl1NQ99qXRR0OZqAWB9mh8Z1HA9bM5K1HZLBTOqA== + dependencies: + "@lerna/validation-error" "4.0.0" + multimatch "^5.0.0" + npmlog "^4.1.2" + +"@lerna/get-npm-exec-opts@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/get-npm-exec-opts/-/get-npm-exec-opts-4.0.0.tgz#dc955be94a4ae75c374ef9bce91320887d34608f" + integrity sha512-yvmkerU31CTWS2c7DvmAWmZVeclPBqI7gPVr5VATUKNWJ/zmVcU4PqbYoLu92I9Qc4gY1TuUplMNdNuZTSL7IQ== + dependencies: + npmlog "^4.1.2" + +"@lerna/get-packed@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/get-packed/-/get-packed-4.0.0.tgz#0989d61624ac1f97e393bdad2137c49cd7a37823" + integrity sha512-rfWONRsEIGyPJTxFzC8ECb3ZbsDXJbfqWYyeeQQDrJRPnEJErlltRLPLgC2QWbxFgFPsoDLeQmFHJnf0iDfd8w== + dependencies: + fs-extra "^9.1.0" + ssri "^8.0.1" + tar "^6.1.0" + +"@lerna/github-client@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/github-client/-/github-client-4.0.0.tgz#2ced67721363ef70f8e12ffafce4410918f4a8a4" + integrity sha512-2jhsldZtTKXYUBnOm23Lb0Fx8G4qfSXF9y7UpyUgWUj+YZYd+cFxSuorwQIgk5P4XXrtVhsUesIsli+BYSThiw== + dependencies: + "@lerna/child-process" "4.0.0" + "@octokit/plugin-enterprise-rest" "^6.0.1" + "@octokit/rest" "^18.1.0" + git-url-parse "^11.4.4" + npmlog "^4.1.2" + +"@lerna/gitlab-client@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/gitlab-client/-/gitlab-client-4.0.0.tgz#00dad73379c7b38951d4b4ded043504c14e2b67d" + integrity sha512-OMUpGSkeDWFf7BxGHlkbb35T7YHqVFCwBPSIR6wRsszY8PAzCYahtH3IaJzEJyUg6vmZsNl0FSr3pdA2skhxqA== + dependencies: + node-fetch "^2.6.1" + npmlog "^4.1.2" + whatwg-url "^8.4.0" + +"@lerna/global-options@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/global-options/-/global-options-4.0.0.tgz#c7d8b0de6a01d8a845e2621ea89e7f60f18c6a5f" + integrity sha512-TRMR8afAHxuYBHK7F++Ogop2a82xQjoGna1dvPOY6ltj/pEx59pdgcJfYcynYqMkFIk8bhLJJN9/ndIfX29FTQ== + +"@lerna/has-npm-version@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/has-npm-version/-/has-npm-version-4.0.0.tgz#d3fc3292c545eb28bd493b36e6237cf0279f631c" + integrity sha512-LQ3U6XFH8ZmLCsvsgq1zNDqka0Xzjq5ibVN+igAI5ccRWNaUsE/OcmsyMr50xAtNQMYMzmpw5GVLAivT2/YzCg== + dependencies: + "@lerna/child-process" "4.0.0" + semver "^7.3.4" + +"@lerna/import@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/import/-/import-4.0.0.tgz#bde656c4a451fa87ae41733ff8a8da60547c5465" + integrity sha512-FaIhd+4aiBousKNqC7TX1Uhe97eNKf5/SC7c5WZANVWtC7aBWdmswwDt3usrzCNpj6/Wwr9EtEbYROzxKH8ffg== + dependencies: + "@lerna/child-process" "4.0.0" + "@lerna/command" "4.0.0" + "@lerna/prompt" "4.0.0" + "@lerna/pulse-till-done" "4.0.0" + "@lerna/validation-error" "4.0.0" + dedent "^0.7.0" + fs-extra "^9.1.0" + p-map-series "^2.1.0" + +"@lerna/info@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/info/-/info-4.0.0.tgz#b9fb0e479d60efe1623603958a831a88b1d7f1fc" + integrity sha512-8Uboa12kaCSZEn4XRfPz5KU9XXoexSPS4oeYGj76s2UQb1O1GdnEyfjyNWoUl1KlJ2i/8nxUskpXIftoFYH0/Q== + dependencies: + "@lerna/command" "4.0.0" + "@lerna/output" "4.0.0" + envinfo "^7.7.4" + +"@lerna/init@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/init/-/init-4.0.0.tgz#dadff67e6dfb981e8ccbe0e6a310e837962f6c7a" + integrity sha512-wY6kygop0BCXupzWj5eLvTUqdR7vIAm0OgyV9WHpMYQGfs1V22jhztt8mtjCloD/O0nEe4tJhdG62XU5aYmPNQ== + dependencies: + "@lerna/child-process" "4.0.0" + "@lerna/command" "4.0.0" + fs-extra "^9.1.0" + p-map "^4.0.0" + write-json-file "^4.3.0" + +"@lerna/link@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/link/-/link-4.0.0.tgz#c3a38aabd44279d714e90f2451e31b63f0fb65ba" + integrity sha512-KlvPi7XTAcVOByfaLlOeYOfkkDcd+bejpHMCd1KcArcFTwijOwXOVi24DYomIeHvy6HsX/IUquJ4PPUJIeB4+w== + dependencies: + "@lerna/command" "4.0.0" + "@lerna/package-graph" "4.0.0" + "@lerna/symlink-dependencies" "4.0.0" + p-map "^4.0.0" + slash "^3.0.0" + +"@lerna/list@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/list/-/list-4.0.0.tgz#24b4e6995bd73f81c556793fe502b847efd9d1d7" + integrity sha512-L2B5m3P+U4Bif5PultR4TI+KtW+SArwq1i75QZ78mRYxPc0U/piau1DbLOmwrdqr99wzM49t0Dlvl6twd7GHFg== + dependencies: + "@lerna/command" "4.0.0" + "@lerna/filter-options" "4.0.0" + "@lerna/listable" "4.0.0" + "@lerna/output" "4.0.0" + +"@lerna/listable@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/listable/-/listable-4.0.0.tgz#d00d6cb4809b403f2b0374fc521a78e318b01214" + integrity sha512-/rPOSDKsOHs5/PBLINZOkRIX1joOXUXEtyUs5DHLM8q6/RP668x/1lFhw6Dx7/U+L0+tbkpGtZ1Yt0LewCLgeQ== + dependencies: + "@lerna/query-graph" "4.0.0" + chalk "^4.1.0" + columnify "^1.5.4" + +"@lerna/log-packed@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/log-packed/-/log-packed-4.0.0.tgz#95168fe2e26ac6a71e42f4be857519b77e57a09f" + integrity sha512-+dpCiWbdzgMAtpajLToy9PO713IHoE6GV/aizXycAyA07QlqnkpaBNZ8DW84gHdM1j79TWockGJo9PybVhrrZQ== + dependencies: + byte-size "^7.0.0" + columnify "^1.5.4" + has-unicode "^2.0.1" + npmlog "^4.1.2" + +"@lerna/npm-conf@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/npm-conf/-/npm-conf-4.0.0.tgz#b259fd1e1cee2bf5402b236e770140ff9ade7fd2" + integrity sha512-uS7H02yQNq3oejgjxAxqq/jhwGEE0W0ntr8vM3EfpCW1F/wZruwQw+7bleJQ9vUBjmdXST//tk8mXzr5+JXCfw== + dependencies: + config-chain "^1.1.12" + pify "^5.0.0" + +"@lerna/npm-dist-tag@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/npm-dist-tag/-/npm-dist-tag-4.0.0.tgz#d1e99b4eccd3414142f0548ad331bf2d53f3257a" + integrity sha512-F20sg28FMYTgXqEQihgoqSfwmq+Id3zT23CnOwD+XQMPSy9IzyLf1fFVH319vXIw6NF6Pgs4JZN2Qty6/CQXGw== + dependencies: + "@lerna/otplease" "4.0.0" + npm-package-arg "^8.1.0" + npm-registry-fetch "^9.0.0" + npmlog "^4.1.2" + +"@lerna/npm-install@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/npm-install/-/npm-install-4.0.0.tgz#31180be3ab3b7d1818a1a0c206aec156b7094c78" + integrity sha512-aKNxq2j3bCH3eXl3Fmu4D54s/YLL9WSwV8W7X2O25r98wzrO38AUN6AB9EtmAx+LV/SP15et7Yueg9vSaanRWg== + dependencies: + "@lerna/child-process" "4.0.0" + "@lerna/get-npm-exec-opts" "4.0.0" + fs-extra "^9.1.0" + npm-package-arg "^8.1.0" + npmlog "^4.1.2" + signal-exit "^3.0.3" + write-pkg "^4.0.0" + +"@lerna/npm-publish@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/npm-publish/-/npm-publish-4.0.0.tgz#84eb62e876fe949ae1fd62c60804423dbc2c4472" + integrity sha512-vQb7yAPRo5G5r77DRjHITc9piR9gvEKWrmfCH7wkfBnGWEqu7n8/4bFQ7lhnkujvc8RXOsYpvbMQkNfkYibD/w== + dependencies: + "@lerna/otplease" "4.0.0" + "@lerna/run-lifecycle" "4.0.0" + fs-extra "^9.1.0" + libnpmpublish "^4.0.0" + npm-package-arg "^8.1.0" + npmlog "^4.1.2" + pify "^5.0.0" + read-package-json "^3.0.0" + +"@lerna/npm-run-script@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/npm-run-script/-/npm-run-script-4.0.0.tgz#dfebf4f4601442e7c0b5214f9fb0d96c9350743b" + integrity sha512-Jmyh9/IwXJjOXqKfIgtxi0bxi1pUeKe5bD3S81tkcy+kyng/GNj9WSqD5ZggoNP2NP//s4CLDAtUYLdP7CU9rA== + dependencies: + "@lerna/child-process" "4.0.0" + "@lerna/get-npm-exec-opts" "4.0.0" + npmlog "^4.1.2" + +"@lerna/otplease@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/otplease/-/otplease-4.0.0.tgz#84972eb43448f8a1077435ba1c5e59233b725850" + integrity sha512-Sgzbqdk1GH4psNiT6hk+BhjOfIr/5KhGBk86CEfHNJTk9BK4aZYyJD4lpDbDdMjIV4g03G7pYoqHzH765T4fxw== + dependencies: + "@lerna/prompt" "4.0.0" + +"@lerna/output@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/output/-/output-4.0.0.tgz#b1d72215c0e35483e4f3e9994debc82c621851f2" + integrity sha512-Un1sHtO1AD7buDQrpnaYTi2EG6sLF+KOPEAMxeUYG5qG3khTs2Zgzq5WE3dt2N/bKh7naESt20JjIW6tBELP0w== + dependencies: + npmlog "^4.1.2" + +"@lerna/pack-directory@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/pack-directory/-/pack-directory-4.0.0.tgz#8b617db95d20792f043aaaa13a9ccc0e04cb4c74" + integrity sha512-NJrmZNmBHS+5aM+T8N6FVbaKFScVqKlQFJNY2k7nsJ/uklNKsLLl6VhTQBPwMTbf6Tf7l6bcKzpy7aePuq9UiQ== + dependencies: + "@lerna/get-packed" "4.0.0" + "@lerna/package" "4.0.0" + "@lerna/run-lifecycle" "4.0.0" + npm-packlist "^2.1.4" + npmlog "^4.1.2" + tar "^6.1.0" + temp-write "^4.0.0" + +"@lerna/package-graph@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/package-graph/-/package-graph-4.0.0.tgz#16a00253a8ac810f72041481cb46bcee8d8123dd" + integrity sha512-QED2ZCTkfXMKFoTGoccwUzjHtZMSf3UKX14A4/kYyBms9xfFsesCZ6SLI5YeySEgcul8iuIWfQFZqRw+Qrjraw== + dependencies: + "@lerna/prerelease-id-from-version" "4.0.0" + "@lerna/validation-error" "4.0.0" + npm-package-arg "^8.1.0" + npmlog "^4.1.2" + semver "^7.3.4" + +"@lerna/package@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/package/-/package-4.0.0.tgz#1b4c259c4bcff45c876ee1d591a043aacbc0d6b7" + integrity sha512-l0M/izok6FlyyitxiQKr+gZLVFnvxRQdNhzmQ6nRnN9dvBJWn+IxxpM+cLqGACatTnyo9LDzNTOj2Db3+s0s8Q== + dependencies: + load-json-file "^6.2.0" + npm-package-arg "^8.1.0" + write-pkg "^4.0.0" + +"@lerna/prerelease-id-from-version@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/prerelease-id-from-version/-/prerelease-id-from-version-4.0.0.tgz#c7e0676fcee1950d85630e108eddecdd5b48c916" + integrity sha512-GQqguzETdsYRxOSmdFZ6zDBXDErIETWOqomLERRY54f4p+tk4aJjoVdd9xKwehC9TBfIFvlRbL1V9uQGHh1opg== + dependencies: + semver "^7.3.4" + +"@lerna/profiler@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/profiler/-/profiler-4.0.0.tgz#8a53ab874522eae15d178402bff90a14071908e9" + integrity sha512-/BaEbqnVh1LgW/+qz8wCuI+obzi5/vRE8nlhjPzdEzdmWmZXuCKyWSEzAyHOJWw1ntwMiww5dZHhFQABuoFz9Q== + dependencies: + fs-extra "^9.1.0" + npmlog "^4.1.2" + upath "^2.0.1" + +"@lerna/project@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/project/-/project-4.0.0.tgz#ff84893935833533a74deff30c0e64ddb7f0ba6b" + integrity sha512-o0MlVbDkD5qRPkFKlBZsXZjoNTWPyuL58564nSfZJ6JYNmgAptnWPB2dQlAc7HWRZkmnC2fCkEdoU+jioPavbg== + dependencies: + "@lerna/package" "4.0.0" + "@lerna/validation-error" "4.0.0" + cosmiconfig "^7.0.0" + dedent "^0.7.0" + dot-prop "^6.0.1" + glob-parent "^5.1.1" + globby "^11.0.2" + load-json-file "^6.2.0" + npmlog "^4.1.2" + p-map "^4.0.0" + resolve-from "^5.0.0" + write-json-file "^4.3.0" + +"@lerna/prompt@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/prompt/-/prompt-4.0.0.tgz#5ec69a803f3f0db0ad9f221dad64664d3daca41b" + integrity sha512-4Ig46oCH1TH5M7YyTt53fT6TuaKMgqUUaqdgxvp6HP6jtdak6+amcsqB8YGz2eQnw/sdxunx84DfI9XpoLj4bQ== + dependencies: + inquirer "^7.3.3" + npmlog "^4.1.2" + +"@lerna/publish@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/publish/-/publish-4.0.0.tgz#f67011305adeba120066a3b6d984a5bb5fceef65" + integrity sha512-K8jpqjHrChH22qtkytA5GRKIVFEtqBF6JWj1I8dWZtHs4Jywn8yB1jQ3BAMLhqmDJjWJtRck0KXhQQKzDK2UPg== + dependencies: + "@lerna/check-working-tree" "4.0.0" + "@lerna/child-process" "4.0.0" + "@lerna/collect-updates" "4.0.0" + "@lerna/command" "4.0.0" + "@lerna/describe-ref" "4.0.0" + "@lerna/log-packed" "4.0.0" + "@lerna/npm-conf" "4.0.0" + "@lerna/npm-dist-tag" "4.0.0" + "@lerna/npm-publish" "4.0.0" + "@lerna/otplease" "4.0.0" + "@lerna/output" "4.0.0" + "@lerna/pack-directory" "4.0.0" + "@lerna/prerelease-id-from-version" "4.0.0" + "@lerna/prompt" "4.0.0" + "@lerna/pulse-till-done" "4.0.0" + "@lerna/run-lifecycle" "4.0.0" + "@lerna/run-topologically" "4.0.0" + "@lerna/validation-error" "4.0.0" + "@lerna/version" "4.0.0" + fs-extra "^9.1.0" + libnpmaccess "^4.0.1" + npm-package-arg "^8.1.0" + npm-registry-fetch "^9.0.0" + npmlog "^4.1.2" + p-map "^4.0.0" + p-pipe "^3.1.0" + pacote "^11.2.6" + semver "^7.3.4" + +"@lerna/pulse-till-done@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/pulse-till-done/-/pulse-till-done-4.0.0.tgz#04bace7d483a8205c187b806bcd8be23d7bb80a3" + integrity sha512-Frb4F7QGckaybRhbF7aosLsJ5e9WuH7h0KUkjlzSByVycxY91UZgaEIVjS2oN9wQLrheLMHl6SiFY0/Pvo0Cxg== + dependencies: + npmlog "^4.1.2" + +"@lerna/query-graph@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/query-graph/-/query-graph-4.0.0.tgz#09dd1c819ac5ee3f38db23931143701f8a6eef63" + integrity sha512-YlP6yI3tM4WbBmL9GCmNDoeQyzcyg1e4W96y/PKMZa5GbyUvkS2+Jc2kwPD+5KcXou3wQZxSPzR3Te5OenaDdg== + dependencies: + "@lerna/package-graph" "4.0.0" + +"@lerna/resolve-symlink@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/resolve-symlink/-/resolve-symlink-4.0.0.tgz#6d006628a210c9b821964657a9e20a8c9a115e14" + integrity sha512-RtX8VEUzqT+uLSCohx8zgmjc6zjyRlh6i/helxtZTMmc4+6O4FS9q5LJas2uGO2wKvBlhcD6siibGt7dIC3xZA== + dependencies: + fs-extra "^9.1.0" + npmlog "^4.1.2" + read-cmd-shim "^2.0.0" + +"@lerna/rimraf-dir@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/rimraf-dir/-/rimraf-dir-4.0.0.tgz#2edf3b62d4eb0ef4e44e430f5844667d551ec25a" + integrity sha512-QNH9ABWk9mcMJh2/muD9iYWBk1oQd40y6oH+f3wwmVGKYU5YJD//+zMiBI13jxZRtwBx0vmBZzkBkK1dR11cBg== + dependencies: + "@lerna/child-process" "4.0.0" + npmlog "^4.1.2" + path-exists "^4.0.0" + rimraf "^3.0.2" + +"@lerna/run-lifecycle@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/run-lifecycle/-/run-lifecycle-4.0.0.tgz#e648a46f9210a9bcd7c391df6844498cb5079334" + integrity sha512-IwxxsajjCQQEJAeAaxF8QdEixfI7eLKNm4GHhXHrgBu185JcwScFZrj9Bs+PFKxwb+gNLR4iI5rpUdY8Y0UdGQ== + dependencies: + "@lerna/npm-conf" "4.0.0" + npm-lifecycle "^3.1.5" + npmlog "^4.1.2" + +"@lerna/run-topologically@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/run-topologically/-/run-topologically-4.0.0.tgz#af846eeee1a09b0c2be0d1bfb5ef0f7b04bb1827" + integrity sha512-EVZw9hGwo+5yp+VL94+NXRYisqgAlj0jWKWtAIynDCpghRxCE5GMO3xrQLmQgqkpUl9ZxQFpICgYv5DW4DksQA== + dependencies: + "@lerna/query-graph" "4.0.0" + p-queue "^6.6.2" + +"@lerna/run@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/run/-/run-4.0.0.tgz#4bc7fda055a729487897c23579694f6183c91262" + integrity sha512-9giulCOzlMPzcZS/6Eov6pxE9gNTyaXk0Man+iCIdGJNMrCnW7Dme0Z229WWP/UoxDKg71F2tMsVVGDiRd8fFQ== + dependencies: + "@lerna/command" "4.0.0" + "@lerna/filter-options" "4.0.0" + "@lerna/npm-run-script" "4.0.0" + "@lerna/output" "4.0.0" + "@lerna/profiler" "4.0.0" + "@lerna/run-topologically" "4.0.0" + "@lerna/timer" "4.0.0" + "@lerna/validation-error" "4.0.0" + p-map "^4.0.0" + +"@lerna/symlink-binary@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/symlink-binary/-/symlink-binary-4.0.0.tgz#21009f62d53a425f136cb4c1a32c6b2a0cc02d47" + integrity sha512-zualodWC4q1QQc1pkz969hcFeWXOsVYZC5AWVtAPTDfLl+TwM7eG/O6oP+Rr3fFowspxo6b1TQ6sYfDV6HXNWA== + dependencies: + "@lerna/create-symlink" "4.0.0" + "@lerna/package" "4.0.0" + fs-extra "^9.1.0" + p-map "^4.0.0" + +"@lerna/symlink-dependencies@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/symlink-dependencies/-/symlink-dependencies-4.0.0.tgz#8910eca084ae062642d0490d8972cf2d98e9ebbd" + integrity sha512-BABo0MjeUHNAe2FNGty1eantWp8u83BHSeIMPDxNq0MuW2K3CiQRaeWT3EGPAzXpGt0+hVzBrA6+OT0GPn7Yuw== + dependencies: + "@lerna/create-symlink" "4.0.0" + "@lerna/resolve-symlink" "4.0.0" + "@lerna/symlink-binary" "4.0.0" + fs-extra "^9.1.0" + p-map "^4.0.0" + p-map-series "^2.1.0" + +"@lerna/timer@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/timer/-/timer-4.0.0.tgz#a52e51bfcd39bfd768988049ace7b15c1fd7a6da" + integrity sha512-WFsnlaE7SdOvjuyd05oKt8Leg3ENHICnvX3uYKKdByA+S3g+TCz38JsNs7OUZVt+ba63nC2nbXDlUnuT2Xbsfg== + +"@lerna/validation-error@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/validation-error/-/validation-error-4.0.0.tgz#af9d62fe8304eaa2eb9a6ba1394f9aa807026d35" + integrity sha512-1rBOM5/koiVWlRi3V6dB863E1YzJS8v41UtsHgMr6gB2ncJ2LsQtMKlJpi3voqcgh41H8UsPXR58RrrpPpufyw== + dependencies: + npmlog "^4.1.2" + +"@lerna/version@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/version/-/version-4.0.0.tgz#532659ec6154d8a8789c5ab53878663e244e3228" + integrity sha512-otUgiqs5W9zGWJZSCCMRV/2Zm2A9q9JwSDS7s/tlKq4mWCYriWo7+wsHEA/nPTMDyYyBO5oyZDj+3X50KDUzeA== + dependencies: + "@lerna/check-working-tree" "4.0.0" + "@lerna/child-process" "4.0.0" + "@lerna/collect-updates" "4.0.0" + "@lerna/command" "4.0.0" + "@lerna/conventional-commits" "4.0.0" + "@lerna/github-client" "4.0.0" + "@lerna/gitlab-client" "4.0.0" + "@lerna/output" "4.0.0" + "@lerna/prerelease-id-from-version" "4.0.0" + "@lerna/prompt" "4.0.0" + "@lerna/run-lifecycle" "4.0.0" + "@lerna/run-topologically" "4.0.0" + "@lerna/validation-error" "4.0.0" + chalk "^4.1.0" + dedent "^0.7.0" + load-json-file "^6.2.0" + minimatch "^3.0.4" + npmlog "^4.1.2" + p-map "^4.0.0" + p-pipe "^3.1.0" + p-reduce "^2.1.0" + p-waterfall "^2.1.1" + semver "^7.3.4" + slash "^3.0.0" + temp-write "^4.0.0" + write-json-file "^4.3.0" + +"@lerna/write-log-file@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@lerna/write-log-file/-/write-log-file-4.0.0.tgz#18221a38a6a307d6b0a5844dd592ad53fa27091e" + integrity sha512-XRG5BloiArpXRakcnPHmEHJp+4AtnhRtpDIHSghmXD5EichI1uD73J7FgPp30mm2pDRq3FdqB0NbwSEsJ9xFQg== + dependencies: + npmlog "^4.1.2" + write-file-atomic "^3.0.3" + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@npmcli/ci-detect@^1.0.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@npmcli/ci-detect/-/ci-detect-1.4.0.tgz#18478bbaa900c37bfbd8a2006a6262c62e8b0fe1" + integrity sha512-3BGrt6FLjqM6br5AhWRKTr3u5GIVkjRYeAFrMp3HjnfICrg4xOrVRwFavKT6tsp++bq5dluL5t8ME/Nha/6c1Q== + +"@npmcli/fs@^1.0.0": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-1.1.1.tgz#72f719fe935e687c56a4faecf3c03d06ba593257" + integrity sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ== + dependencies: + "@gar/promisify" "^1.0.1" + semver "^7.3.5" + +"@npmcli/git@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@npmcli/git/-/git-2.1.0.tgz#2fbd77e147530247d37f325930d457b3ebe894f6" + integrity sha512-/hBFX/QG1b+N7PZBFs0bi+evgRZcK9nWBxQKZkGoXUT5hJSwl5c4d7y8/hm+NQZRPhQ67RzFaj5UM9YeyKoryw== + dependencies: + "@npmcli/promise-spawn" "^1.3.2" + lru-cache "^6.0.0" + mkdirp "^1.0.4" + npm-pick-manifest "^6.1.1" + promise-inflight "^1.0.1" + promise-retry "^2.0.1" + semver "^7.3.5" + which "^2.0.2" + +"@npmcli/installed-package-contents@^1.0.6": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@npmcli/installed-package-contents/-/installed-package-contents-1.0.7.tgz#ab7408c6147911b970a8abe261ce512232a3f4fa" + integrity sha512-9rufe0wnJusCQoLpV9ZPKIVP55itrM5BxOXs10DmdbRfgWtHy1LDyskbwRnBghuB0PrF7pNPOqREVtpz4HqzKw== + dependencies: + npm-bundled "^1.1.1" + npm-normalize-package-bin "^1.0.1" + +"@npmcli/move-file@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-1.1.2.tgz#1a82c3e372f7cae9253eb66d72543d6b8685c674" + integrity sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg== + dependencies: + mkdirp "^1.0.4" + rimraf "^3.0.2" + +"@npmcli/node-gyp@^1.0.2": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@npmcli/node-gyp/-/node-gyp-1.0.3.tgz#a912e637418ffc5f2db375e93b85837691a43a33" + integrity sha512-fnkhw+fmX65kiLqk6E3BFLXNC26rUhK90zVwe2yncPliVT/Qos3xjhTLE59Df8KnPlcwIERXKVlU1bXoUQ+liA== + +"@npmcli/promise-spawn@^1.2.0", "@npmcli/promise-spawn@^1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@npmcli/promise-spawn/-/promise-spawn-1.3.2.tgz#42d4e56a8e9274fba180dabc0aea6e38f29274f5" + integrity sha512-QyAGYo/Fbj4MXeGdJcFzZ+FkDkomfRBrPM+9QYJSg+PxgAUL+LU3FneQk37rKR2/zjqkCV1BLHccX98wRXG3Sg== + dependencies: + infer-owner "^1.0.4" + +"@npmcli/run-script@^1.8.2": + version "1.8.6" + resolved "https://registry.yarnpkg.com/@npmcli/run-script/-/run-script-1.8.6.tgz#18314802a6660b0d4baa4c3afe7f1ad39d8c28b7" + integrity sha512-e42bVZnC6VluBZBAFEr3YrdqSspG3bgilyg4nSLBJ7TRGNCzxHa92XAHxQBLYg0BmgwO4b2mf3h/l5EkEWRn3g== + dependencies: + "@npmcli/node-gyp" "^1.0.2" + "@npmcli/promise-spawn" "^1.3.2" + node-gyp "^7.1.0" + read-package-json-fast "^2.0.1" + +"@octokit/auth-token@^2.4.4": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.5.0.tgz#27c37ea26c205f28443402477ffd261311f21e36" + integrity sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g== + dependencies: + "@octokit/types" "^6.0.3" + +"@octokit/core@^3.5.1": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.6.0.tgz#3376cb9f3008d9b3d110370d90e0a1fcd5fe6085" + integrity sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q== + dependencies: + "@octokit/auth-token" "^2.4.4" + "@octokit/graphql" "^4.5.8" + "@octokit/request" "^5.6.3" + "@octokit/request-error" "^2.0.5" + "@octokit/types" "^6.0.3" + before-after-hook "^2.2.0" + universal-user-agent "^6.0.0" + +"@octokit/endpoint@^6.0.1": + version "6.0.12" + resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-6.0.12.tgz#3b4d47a4b0e79b1027fb8d75d4221928b2d05658" + integrity sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA== + dependencies: + "@octokit/types" "^6.0.3" + is-plain-object "^5.0.0" + universal-user-agent "^6.0.0" + +"@octokit/graphql@^4.5.8": + version "4.8.0" + resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-4.8.0.tgz#664d9b11c0e12112cbf78e10f49a05959aa22cc3" + integrity sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg== + dependencies: + "@octokit/request" "^5.6.0" + "@octokit/types" "^6.0.3" + universal-user-agent "^6.0.0" + +"@octokit/openapi-types@^11.2.0": + version "11.2.0" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-11.2.0.tgz#b38d7fc3736d52a1e96b230c1ccd4a58a2f400a6" + integrity sha512-PBsVO+15KSlGmiI8QAzaqvsNlZlrDlyAJYcrXBCvVUxCp7VnXjkwPoFHgjEJXx3WF9BAwkA6nfCUA7i9sODzKA== + +"@octokit/plugin-enterprise-rest@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-6.0.1.tgz#e07896739618dab8da7d4077c658003775f95437" + integrity sha512-93uGjlhUD+iNg1iWhUENAtJata6w5nE+V4urXOAlIXdco6xNZtUSfYY8dzp3Udy74aqO/B5UZL80x/YMa5PKRw== + +"@octokit/plugin-paginate-rest@^2.16.8": + version "2.17.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.17.0.tgz#32e9c7cab2a374421d3d0de239102287d791bce7" + integrity sha512-tzMbrbnam2Mt4AhuyCHvpRkS0oZ5MvwwcQPYGtMv4tUa5kkzG58SVB0fcsLulOZQeRnOgdkZWkRUiyBlh0Bkyw== + dependencies: + "@octokit/types" "^6.34.0" + +"@octokit/plugin-request-log@^1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz#5e50ed7083a613816b1e4a28aeec5fb7f1462e85" + integrity sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA== + +"@octokit/plugin-rest-endpoint-methods@^5.12.0": + version "5.13.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.13.0.tgz#8c46109021a3412233f6f50d28786f8e552427ba" + integrity sha512-uJjMTkN1KaOIgNtUPMtIXDOjx6dGYysdIFhgA52x4xSadQCz3b/zJexvITDVpANnfKPW/+E0xkOvLntqMYpviA== + dependencies: + "@octokit/types" "^6.34.0" + deprecation "^2.3.1" + +"@octokit/request-error@^2.0.5", "@octokit/request-error@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-2.1.0.tgz#9e150357831bfc788d13a4fd4b1913d60c74d677" + integrity sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg== + dependencies: + "@octokit/types" "^6.0.3" + deprecation "^2.0.0" + once "^1.4.0" + +"@octokit/request@^5.6.0", "@octokit/request@^5.6.3": + version "5.6.3" + resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.6.3.tgz#19a022515a5bba965ac06c9d1334514eb50c48b0" + integrity sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A== + dependencies: + "@octokit/endpoint" "^6.0.1" + "@octokit/request-error" "^2.1.0" + "@octokit/types" "^6.16.1" + is-plain-object "^5.0.0" + node-fetch "^2.6.7" + universal-user-agent "^6.0.0" + +"@octokit/rest@^18.1.0": + version "18.12.0" + resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-18.12.0.tgz#f06bc4952fc87130308d810ca9d00e79f6988881" + integrity sha512-gDPiOHlyGavxr72y0guQEhLsemgVjwRePayJ+FcKc2SJqKUbxbkvf5kAZEWA/MKvsfYlQAMVzNJE3ezQcxMJ2Q== + dependencies: + "@octokit/core" "^3.5.1" + "@octokit/plugin-paginate-rest" "^2.16.8" + "@octokit/plugin-request-log" "^1.0.4" + "@octokit/plugin-rest-endpoint-methods" "^5.12.0" + +"@octokit/types@^6.0.3", "@octokit/types@^6.16.1", "@octokit/types@^6.34.0": + version "6.34.0" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.34.0.tgz#c6021333334d1ecfb5d370a8798162ddf1ae8218" + integrity sha512-s1zLBjWhdEI2zwaoSgyOFoKSl109CUcVBCc7biPJ3aAf6LGLU6szDvi31JPU7bxfla2lqfhjbbg/5DdFNxOwHw== + dependencies: + "@octokit/openapi-types" "^11.2.0" + +"@sinonjs/commons@^1.7.0": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d" + integrity sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@^8.0.1": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz#3fdc2b6cb58935b21bfb8d1625eb1300484316e7" + integrity sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg== + dependencies: + "@sinonjs/commons" "^1.7.0" + +"@tootallnate/once@1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" + integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== + +"@tsconfig/node10@^1.0.7": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9" + integrity sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg== + +"@tsconfig/node12@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.9.tgz#62c1f6dee2ebd9aead80dc3afa56810e58e1a04c" + integrity sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw== + +"@tsconfig/node14@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2" + integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg== + +"@tsconfig/node16@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e" + integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA== + +"@types/aws-lambda@8.10.86": + version "8.10.86" + resolved "https://registry.yarnpkg.com/@types/aws-lambda/-/aws-lambda-8.10.86.tgz#f97d9e2f75f87b03401bcd19737b025ec08de200" + integrity sha512-CH7MtDqW8hAm05alc837XHvqKcrfx/kacc4EyUoyN5PoFK3D0lIfN0GK2WsV6YKU4tUsLTLAVLSyBrcFdHP9Bw== + +"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14": + version "7.1.19" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.19.tgz#7b497495b7d1b4812bdb9d02804d0576f43ee460" + integrity sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.6.4" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.4.tgz#1f20ce4c5b1990b37900b63f050182d28c2439b7" + integrity sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.4.1" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.1.tgz#3d1a48fd9d6c0edfd56f2ff578daed48f36c8969" + integrity sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*", "@types/babel__traverse@^7.0.4", "@types/babel__traverse@^7.0.6": + version "7.14.2" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.14.2.tgz#ffcd470bbb3f8bf30481678fb5502278ca833a43" + integrity sha512-K2waXdXBi2302XUdcHcR1jCeU0LL4TD9HRs/gk0N2Xvrht+G/BfJa4QObBQZfhMdxiCpV3COl5Nfq4uKTeTnJA== + dependencies: + "@babel/types" "^7.3.0" + +"@types/fs-extra@9.0.13": + version "9.0.13" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.13.tgz#7594fbae04fe7f1918ce8b3d213f74ff44ac1f45" + integrity sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA== + dependencies: + "@types/node" "*" + +"@types/graceful-fs@^4.1.2": + version "4.1.5" + resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15" + integrity sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw== + dependencies: + "@types/node" "*" + +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" + integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g== + +"@types/istanbul-lib-report@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686" + integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg== + dependencies: + "@types/istanbul-lib-coverage" "*" + +"@types/istanbul-reports@^3.0.0": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz#9153fe98bba2bd565a63add9436d6f0d7f8468ff" + integrity sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw== + dependencies: + "@types/istanbul-lib-report" "*" + +"@types/jest@27.0.3": + version "27.0.3" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.0.3.tgz#0cf9dfe9009e467f70a342f0f94ead19842a783a" + integrity sha512-cmmwv9t7gBYt7hNKH5Spu7Kuu/DotGa+Ff+JGRKZ4db5eh8PnKS4LuebJ3YLUoyOyIHraTGyULn23YtEAm0VSg== + dependencies: + jest-diff "^27.0.0" + pretty-format "^27.0.0" + +"@types/js-yaml@4.0.5": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.5.tgz#738dd390a6ecc5442f35e7f03fa1431353f7e138" + integrity sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA== + +"@types/json-schema@^7.0.9": + version "7.0.10" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.10.tgz#9b05b7896166cd00e9cbd59864853abf65d9ac23" + integrity sha512-BLO9bBq59vW3fxCpD4o0N4U+DXsvwvIcl+jofw0frQo/GrBFC+/jRZj1E7kgp6dvTyNmA4y6JCV5Id/r3mNP5A== + +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= + +"@types/minimatch@^3.0.3": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40" + integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ== + +"@types/minimist@^1.2.0": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c" + integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== + +"@types/mri@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/mri/-/mri-1.1.1.tgz#bb2fb3f21068998e816fcdd2bcefd4233a711e6e" + integrity sha512-nJOuiTlsvmClSr3+a/trTSx4DTuY/VURsWGKSf/eeavh0LRMqdsK60ti0TlwM5iHiGOK3/Ibkxsbr7i9rzGreA== + +"@types/node@*": + version "17.0.22" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.22.tgz#38b6c4b9b2f3ed9f2e376cce42a298fb2375251e" + integrity sha512-8FwbVoG4fy+ykY86XCAclKZDORttqE5/s7dyWZKLXTdv3vRy5HozBEinG5IqhvPXXzIZEcTVbuHlQEI6iuwcmw== + +"@types/node@16.11.12": + version "16.11.12" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.12.tgz#ac7fb693ac587ee182c3780c26eb65546a1a3c10" + integrity sha512-+2Iggwg7PxoO5Kyhvsq9VarmPbIelXP070HMImEpbtGCoyWNINQj4wzjbQCXzdHTRXnqufutJb5KAURZANNBAw== + +"@types/node@^10.17.60": + version "10.17.60" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b" + integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw== + +"@types/normalize-package-data@^2.4.0": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" + integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== + +"@types/parse-json@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" + integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== + +"@types/prettier@^2.1.5": + version "2.4.4" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.4.4.tgz#5d9b63132df54d8909fce1c3f8ca260fdd693e17" + integrity sha512-ReVR2rLTV1kvtlWFyuot+d1pkpG2Fw/XKE3PDAdj57rbM97ttSp9JZ2UsP+2EHTylra9cUf6JA7tGwW1INzUrA== + +"@types/promptly@3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/promptly/-/promptly-3.0.2.tgz#598674d4b78b3dffcb2d756b344f28a2cf7459f8" + integrity sha512-cJFwE7d8GlraY+DJoZ0NhpoJ55slkcbNsGIKMY0H+5h0xaGqXBqXz9zeu+Ey9KfN1UiHQXiIT0GroxyPYMPP/w== + dependencies: + "@types/node" "*" + +"@types/semver@7.3.9": + version "7.3.9" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.9.tgz#152c6c20a7688c30b967ec1841d31ace569863fc" + integrity sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ== + +"@types/stack-utils@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" + integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== + +"@types/uuid@8.3.3": + version "8.3.3" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.3.tgz#c6a60686d953dbd1b1d45e66f4ecdbd5d471b4d0" + integrity sha512-0LbEEx1zxrYB3pgpd1M5lEhLcXjKJnYghvhTRgaBeUivLHMDM1TzF3IJ6hXU2+8uA4Xz+5BA63mtZo5DjVT8iA== + +"@types/yargs-parser@*": + version "21.0.0" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" + integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== + +"@types/yargs@^16.0.0": + version "16.0.4" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-16.0.4.tgz#26aad98dd2c2a38e421086ea9ad42b9e51642977" + integrity sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw== + dependencies: + "@types/yargs-parser" "*" + +"@typescript-eslint/eslint-plugin@5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.6.0.tgz#efd8668b3d6627c46ce722c2afe813928fe120a0" + integrity sha512-MIbeMy5qfLqtgs1hWd088k1hOuRsN9JrHUPwVVKCD99EOUqScd7SrwoZl4Gso05EAP9w1kvLWUVGJOVpRPkDPA== + dependencies: + "@typescript-eslint/experimental-utils" "5.6.0" + "@typescript-eslint/scope-manager" "5.6.0" + debug "^4.3.2" + functional-red-black-tree "^1.0.1" + ignore "^5.1.8" + regexpp "^3.2.0" + semver "^7.3.5" + tsutils "^3.21.0" + +"@typescript-eslint/experimental-utils@5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.6.0.tgz#f3a5960f2004abdcac7bb81412bafc1560841c23" + integrity sha512-VDoRf3Qj7+W3sS/ZBXZh3LBzp0snDLEgvp6qj0vOAIiAPM07bd5ojQ3CTzF/QFl5AKh7Bh1ycgj6lFBJHUt/DA== + dependencies: + "@types/json-schema" "^7.0.9" + "@typescript-eslint/scope-manager" "5.6.0" + "@typescript-eslint/types" "5.6.0" + "@typescript-eslint/typescript-estree" "5.6.0" + eslint-scope "^5.1.1" + eslint-utils "^3.0.0" + +"@typescript-eslint/experimental-utils@^5.0.0": + version "5.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.16.0.tgz#0b852567efa10660047f281cf004ed3db32866da" + integrity sha512-bitZtqO13XX64/UOQKoDbVg2H4VHzbHnWWlTRc7ofq7SuQyPCwEycF1Zmn5ZAMTJZ3p5uMS7xJGUdOtZK7LrNw== + dependencies: + "@typescript-eslint/utils" "5.16.0" + +"@typescript-eslint/parser@5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.6.0.tgz#11677324659641400d653253c03dcfbed468d199" + integrity sha512-YVK49NgdUPQ8SpCZaOpiq1kLkYRPMv9U5gcMrywzI8brtwZjr/tG3sZpuHyODt76W/A0SufNjYt9ZOgrC4tLIQ== + dependencies: + "@typescript-eslint/scope-manager" "5.6.0" + "@typescript-eslint/types" "5.6.0" + "@typescript-eslint/typescript-estree" "5.6.0" + debug "^4.3.2" + +"@typescript-eslint/scope-manager@5.16.0": + version "5.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.16.0.tgz#7e7909d64bd0c4d8aef629cdc764b9d3e1d3a69a" + integrity sha512-P+Yab2Hovg8NekLIR/mOElCDPyGgFZKhGoZA901Yax6WR6HVeGLbsqJkZ+Cvk5nts/dAlFKm8PfL43UZnWdpIQ== + dependencies: + "@typescript-eslint/types" "5.16.0" + "@typescript-eslint/visitor-keys" "5.16.0" + +"@typescript-eslint/scope-manager@5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.6.0.tgz#9dd7f007dc8f3a34cdff6f79f5eaab27ae05157e" + integrity sha512-1U1G77Hw2jsGWVsO2w6eVCbOg0HZ5WxL/cozVSTfqnL/eB9muhb8THsP0G3w+BB5xAHv9KptwdfYFAUfzcIh4A== + dependencies: + "@typescript-eslint/types" "5.6.0" + "@typescript-eslint/visitor-keys" "5.6.0" + +"@typescript-eslint/types@5.16.0": + version "5.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.16.0.tgz#5827b011982950ed350f075eaecb7f47d3c643ee" + integrity sha512-oUorOwLj/3/3p/HFwrp6m/J2VfbLC8gjW5X3awpQJ/bSG+YRGFS4dpsvtQ8T2VNveV+LflQHjlLvB6v0R87z4g== + +"@typescript-eslint/types@5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.6.0.tgz#745cb1b59daadcc1f32f7be95f0f68accf38afdd" + integrity sha512-OIZffked7mXv4mXzWU5MgAEbCf9ecNJBKi+Si6/I9PpTaj+cf2x58h2oHW5/P/yTnPkKaayfjhLvx+crnl5ubA== + +"@typescript-eslint/typescript-estree@5.16.0": + version "5.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.16.0.tgz#32259459ec62f5feddca66adc695342f30101f61" + integrity sha512-SE4VfbLWUZl9MR+ngLSARptUv2E8brY0luCdgmUevU6arZRY/KxYoLI/3V/yxaURR8tLRN7bmZtJdgmzLHI6pQ== + dependencies: + "@typescript-eslint/types" "5.16.0" + "@typescript-eslint/visitor-keys" "5.16.0" + debug "^4.3.2" + globby "^11.0.4" + is-glob "^4.0.3" + semver "^7.3.5" + tsutils "^3.21.0" + +"@typescript-eslint/typescript-estree@5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.6.0.tgz#dfbb19c9307fdd81bd9c650c67e8397821d7faf0" + integrity sha512-92vK5tQaE81rK7fOmuWMrSQtK1IMonESR+RJR2Tlc7w4o0MeEdjgidY/uO2Gobh7z4Q1hhS94Cr7r021fMVEeA== + dependencies: + "@typescript-eslint/types" "5.6.0" + "@typescript-eslint/visitor-keys" "5.6.0" + debug "^4.3.2" + globby "^11.0.4" + is-glob "^4.0.3" + semver "^7.3.5" + tsutils "^3.21.0" + +"@typescript-eslint/utils@5.16.0": + version "5.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.16.0.tgz#42218b459d6d66418a4eb199a382bdc261650679" + integrity sha512-iYej2ER6AwmejLWMWzJIHy3nPJeGDuCqf8Jnb+jAQVoPpmWzwQOfa9hWVB8GIQE5gsCv/rfN4T+AYb/V06WseQ== + dependencies: + "@types/json-schema" "^7.0.9" + "@typescript-eslint/scope-manager" "5.16.0" + "@typescript-eslint/types" "5.16.0" + "@typescript-eslint/typescript-estree" "5.16.0" + eslint-scope "^5.1.1" + eslint-utils "^3.0.0" + +"@typescript-eslint/visitor-keys@5.16.0": + version "5.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.16.0.tgz#f27dc3b943e6317264c7492e390c6844cd4efbbb" + integrity sha512-jqxO8msp5vZDhikTwq9ubyMHqZ67UIvawohr4qF3KhlpL7gzSjOd+8471H3nh5LyABkaI85laEKKU8SnGUK5/g== + dependencies: + "@typescript-eslint/types" "5.16.0" + eslint-visitor-keys "^3.0.0" + +"@typescript-eslint/visitor-keys@5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.6.0.tgz#3e36509e103fe9713d8f035ac977235fd63cb6e6" + integrity sha512-1p7hDp5cpRFUyE3+lvA74egs+RWSgumrBpzBCDzfTFv0aQ7lIeay80yU0hIxgAhwQ6PcasW35kaOCyDOv6O/Ng== + dependencies: + "@typescript-eslint/types" "5.6.0" + eslint-visitor-keys "^3.0.0" + +"@xmldom/xmldom@^0.8.1": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.1.tgz#70c239275fc6d6a84e41b9a8d623a93c0d59b6b4" + integrity sha512-4wOae+5N2RZ+CZXd9ZKwkaDi55IxrSTOjHpxTvQQ4fomtOJmqVxbmICA9jE1jvnqNhpfgz8cnfFagG86wV/xLQ== + +JSONStream@^1.0.4: + version "1.3.5" + resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" + integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ== + dependencies: + jsonparse "^1.2.0" + through ">=2.2.7 <3" + +abab@^2.0.3, abab@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" + integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q== + +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + +acorn-globals@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45" + integrity sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg== + dependencies: + acorn "^7.1.1" + acorn-walk "^7.1.1" + +acorn-jsx@^5.3.1: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn-walk@^7.1.1: + version "7.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" + integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== + +acorn-walk@^8.1.1, acorn-walk@^8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" + integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + +acorn@^7.1.1: + version "7.4.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" + integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== + +acorn@^8.2.4, acorn@^8.4.1, acorn@^8.7.0: + version "8.7.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf" + integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ== + +add-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/add-stream/-/add-stream-1.0.0.tgz#6a7990437ca736d5e1288db92bd3266d5f5cb2aa" + integrity sha1-anmQQ3ynNtXhKI25K9MmbV9csqo= + +agent-base@6, agent-base@^6.0.0, agent-base@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + +agentkeepalive@^4.1.3: + version "4.2.1" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.2.1.tgz#a7975cbb9f83b367f06c90cc51ff28fe7d499717" + integrity sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA== + dependencies: + debug "^4.1.0" + depd "^1.1.2" + humanize-ms "^1.2.1" + +aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + +ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ajv@^8.0.1: + version "8.10.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.10.0.tgz#e573f719bd3af069017e3b66538ab968d040e54d" + integrity sha512-bzqAEZOjkrUMl2afH8dknrq5KEk2SrwdBROR+vH1EKVQTqaUbJVPdc/gEdggTMM0Se+s+Ja4ju4TlNcStKl2Hw== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + +ansi-colors@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + +ansi-escapes@^4.2.1, ansi-escapes@^4.3.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-regex@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" + integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + +ansi-styles@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.1.0.tgz#87313c102b8118abd57371afab34618bf7350ed3" + integrity sha512-VbqNsoz55SYGczauuup0MFUyXNQviSpFTj1RQtFzmQLk18qbVSpTFFGMT293rmDaQuKCT6InmbuEyUne4mTuxQ== + +anymatch@^3.0.3, anymatch@~3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" + integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +aproba@^1.0.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== + +aproba@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" + integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== + +archiver-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-2.1.0.tgz#e8a460e94b693c3e3da182a098ca6285ba9249e2" + integrity sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw== + dependencies: + glob "^7.1.4" + graceful-fs "^4.2.0" + lazystream "^1.0.0" + lodash.defaults "^4.2.0" + lodash.difference "^4.5.0" + lodash.flatten "^4.4.0" + lodash.isplainobject "^4.0.6" + lodash.union "^4.6.0" + normalize-path "^3.0.0" + readable-stream "^2.0.0" + +archiver@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/archiver/-/archiver-5.3.0.tgz#dd3e097624481741df626267564f7dd8640a45ba" + integrity sha512-iUw+oDwK0fgNpvveEsdQ0Ase6IIKztBJU2U0E9MzszMfmVVUyv1QJhS2ITW9ZCqx8dktAxVAjWWkKehuZE8OPg== + dependencies: + archiver-utils "^2.1.0" + async "^3.2.0" + buffer-crc32 "^0.2.1" + readable-stream "^3.6.0" + readdir-glob "^1.0.0" + tar-stream "^2.2.0" + zip-stream "^4.1.0" + +are-we-there-yet@~1.1.2: + version "1.1.7" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz#b15474a932adab4ff8a50d9adfa7e4e926f21146" + integrity sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g== + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +array-differ@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b" + integrity sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg== + +array-ify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece" + integrity sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4= + +array-includes@^3.1.4: + version "3.1.4" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.4.tgz#f5b493162c760f3539631f005ba2bb46acb45ba9" + integrity sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.1" + get-intrinsic "^1.1.1" + is-string "^1.0.7" + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +array.prototype.flat@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.5.tgz#07e0975d84bbc7c48cd1879d609e682598d33e13" + integrity sha512-KaYU+S+ndVqyUnignHftkwc58o3uVU1jzczILJ1tN2YaIZpFIKBiP/x/j97E5MVPsaCloPbqWLB/8qCTVvT2qg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.0" + +arrify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= + +arrify@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" + integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== + +asap@^2.0.0: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= + +asn1@~0.2.3: + version "0.2.6" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" + integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= + +ast-types@^0.13.2: + version "0.13.4" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.13.4.tgz#ee0d77b343263965ecc3fb62da16e7222b2b6782" + integrity sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w== + dependencies: + tslib "^2.0.1" + +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== + +async@^3.1.0, async@^3.2.0: + version "3.2.3" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9" + integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + +at-least-node@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" + integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== + +available-typed-arrays@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" + integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== + +aws-cdk-lib@2.16.0: + version "2.16.0" + resolved "https://registry.yarnpkg.com/aws-cdk-lib/-/aws-cdk-lib-2.16.0.tgz#51b2d07bac27f5401fda7ccc52d08331289f8558" + integrity sha512-qQlSNat5/HPNFbExZ8LTVDLOZaI+8O2Uv6/vsp87gqfapTqu6MM9m7MouTnFd+f3JiAdTBDu7A0q802oZSH6JA== + dependencies: + "@balena/dockerignore" "^1.0.2" + case "1.6.3" + fs-extra "^9.1.0" + ignore "^5.2.0" + jsonschema "^1.4.0" + minimatch "^3.1.2" + punycode "^2.1.1" + semver "^7.3.5" + yaml "1.10.2" + +aws-cdk@2.16.0: + version "2.16.0" + resolved "https://registry.yarnpkg.com/aws-cdk/-/aws-cdk-2.16.0.tgz#953610e7bbd398dc1f1771f9c9a99595d84214e3" + integrity sha512-7pX1FNY2lhBwFJwrD4b0fTfbaYqLgxiGrd0KEu3oOk2yAR/GFaSo9L83jT7fdNzpZE1GzB4Ryd6svCCyPLE18A== + optionalDependencies: + fsevents "2.3.2" + +aws-lambda@1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/aws-lambda/-/aws-lambda-1.0.7.tgz#c6b674df47458b5ecd43ab734899ad2e2d457013" + integrity sha512-9GNFMRrEMG5y3Jvv+V4azWvc+qNWdWLTjDdhf/zgMlz8haaaLWv0xeAIWxz9PuWUBawsVxy0zZotjCdR3Xq+2w== + dependencies: + aws-sdk "^2.814.0" + commander "^3.0.2" + js-yaml "^3.14.1" + watchpack "^2.0.0-beta.10" + +aws-sdk@2.1001.0: + version "2.1001.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1001.0.tgz#c4da256aa0058438ba611ae06fa850f4f7d63abc" + integrity sha512-DpmslPU8myCaaRUwMzB/SqAMtD2zQckxYwq3CguIv8BI+JHxDLeTdPCLfA5jffQ8k6dcvISOuiqdpwCZucU0BA== + dependencies: + buffer "4.9.2" + events "1.1.1" + ieee754 "1.1.13" + jmespath "0.15.0" + querystring "0.2.0" + sax "1.2.1" + url "0.10.3" + uuid "3.3.2" + xml2js "0.4.19" + +aws-sdk@2.1046.0: + version "2.1046.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1046.0.tgz#9147b0fa1c86acbebd1a061e951ab5012f4499d7" + integrity sha512-ocwHclMXdIA+NWocUyvp9Ild3/zy2vr5mHp3mTyodf0WU5lzBE8PocCVLSWhMAXLxyia83xv2y5f5AzAcetbqA== + dependencies: + buffer "4.9.2" + events "1.1.1" + ieee754 "1.1.13" + jmespath "0.15.0" + querystring "0.2.0" + sax "1.2.1" + url "0.10.3" + uuid "3.3.2" + xml2js "0.4.19" + +aws-sdk@2.1101.0: + version "2.1101.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1101.0.tgz#67b685436d909bbe5c608ca890a4abe1128dd573" + integrity sha512-7lyVb7GXGl8yyu954Qxf6vU6MrcgFlmKyTLBVXJyo3Phn1OB+qOExA55WtSC6gQiQ7e5TeWOn1RUHLg30ywTBA== + dependencies: + buffer "4.9.2" + events "1.1.1" + ieee754 "1.1.13" + jmespath "0.16.0" + querystring "0.2.0" + sax "1.2.1" + url "0.10.3" + uuid "3.3.2" + xml2js "0.4.19" + +aws-sdk@2.944.0: + version "2.944.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.944.0.tgz#5cf062b76bb42eee1a5c31dea82bec1e241488d5" + integrity sha512-QHKDbs/og1kUhOZsE6A8KN/Rw3LHkfEWBqziRaWDNoijDZzY9XlbLHnLmpkluCe4HNKY1KOOyBajhf2CkJahcQ== + dependencies: + buffer "4.9.2" + events "1.1.1" + ieee754 "1.1.13" + jmespath "0.15.0" + querystring "0.2.0" + sax "1.2.1" + url "0.10.3" + uuid "3.3.2" + xml2js "0.4.19" + +aws-sdk@^2.814.0, aws-sdk@^2.848.0: + version "2.1098.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1098.0.tgz#54f8c1592b57e545c3d010f68dc70f5c2ee9ebb5" + integrity sha512-h81SAgqAxij1db9zbFAXEneaC1LNcx/xIIf13tF/IutdYOzd7mQ3lHFCgjp+fB1Msl8z73Vs0VGstyrhK7+zPw== + dependencies: + buffer "4.9.2" + events "1.1.1" + ieee754 "1.1.13" + jmespath "0.16.0" + querystring "0.2.0" + sax "1.2.1" + url "0.10.3" + uuid "3.3.2" + xml2js "0.4.19" + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= + +aws4@^1.8.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" + integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== + +babel-jest@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.5.1.tgz#a1bf8d61928edfefd21da27eb86a695bfd691444" + integrity sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg== + dependencies: + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/babel__core" "^7.1.14" + babel-plugin-istanbul "^6.1.1" + babel-preset-jest "^27.5.1" + chalk "^4.0.0" + graceful-fs "^4.2.9" + slash "^3.0.0" + +babel-plugin-istanbul@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" + integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@istanbuljs/load-nyc-config" "^1.0.0" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-instrument "^5.0.4" + test-exclude "^6.0.0" + +babel-plugin-jest-hoist@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz#9be98ecf28c331eb9f5df9c72d6f89deb8181c2e" + integrity sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ== + dependencies: + "@babel/template" "^7.3.3" + "@babel/types" "^7.3.3" + "@types/babel__core" "^7.0.0" + "@types/babel__traverse" "^7.0.6" + +babel-preset-current-node-syntax@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b" + integrity sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ== + dependencies: + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-bigint" "^7.8.3" + "@babel/plugin-syntax-class-properties" "^7.8.3" + "@babel/plugin-syntax-import-meta" "^7.8.3" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.8.3" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.8.3" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-top-level-await" "^7.8.3" + +babel-preset-jest@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz#91f10f58034cb7989cb4f962b69fa6eef6a6bc81" + integrity sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag== + dependencies: + babel-plugin-jest-hoist "^27.5.1" + babel-preset-current-node-syntax "^1.0.0" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base64-js@^1.0.2, base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= + dependencies: + tweetnacl "^0.14.3" + +before-after-hook@^2.2.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.2.tgz#a6e8ca41028d90ee2c24222f201c90956091613e" + integrity sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ== + +big-integer@^1.6.48: + version "1.6.51" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686" + integrity sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg== + +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + +bl@^4.0.3: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + +bowser@^2.11.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f" + integrity sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^3.0.1, braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +browser-process-hrtime@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" + integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== + +browserslist@^4.17.5: + version "4.20.2" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.20.2.tgz#567b41508757ecd904dab4d1c646c612cd3d4f88" + integrity sha512-CQOBCqp/9pDvDbx3xfMi+86pr4KXIf2FDkTTdeuYw8OxS9t898LA1Khq57gtufFILXpfgsSx5woNgsBgvGjpsA== + dependencies: + caniuse-lite "^1.0.30001317" + electron-to-chromium "^1.4.84" + escalade "^3.1.1" + node-releases "^2.0.2" + picocolors "^1.0.0" + +bs-logger@0.x: + version "0.2.6" + resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" + integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== + dependencies: + fast-json-stable-stringify "2.x" + +bser@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" + integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== + dependencies: + node-int64 "^0.4.0" + +buffer-crc32@^0.2.1, buffer-crc32@^0.2.13: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +buffer@4.9.2: + version "4.9.2" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" + integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + +buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + +builtins@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88" + integrity sha1-y5T662HIaWRR2zZTThQi+U8K7og= + +byline@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1" + integrity sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE= + +byte-size@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/byte-size/-/byte-size-7.0.1.tgz#b1daf3386de7ab9d706b941a748dbfc71130dee3" + integrity sha512-crQdqyCwhokxwV1UyDzLZanhkugAgft7vt0qbbdt60C6Zf3CAiGmtUCylbtYwrU6loOUw3euGrNtW1J651ot1A== + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +cacache@^15.0.5, cacache@^15.2.0: + version "15.3.0" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.3.0.tgz#dc85380fb2f556fe3dda4c719bfa0ec875a7f1eb" + integrity sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ== + dependencies: + "@npmcli/fs" "^1.0.0" + "@npmcli/move-file" "^1.0.1" + chownr "^2.0.0" + fs-minipass "^2.0.0" + glob "^7.1.4" + infer-owner "^1.0.4" + lru-cache "^6.0.0" + minipass "^3.1.1" + minipass-collect "^1.0.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.2" + mkdirp "^1.0.3" + p-map "^4.0.0" + promise-inflight "^1.0.1" + rimraf "^3.0.2" + ssri "^8.0.1" + tar "^6.0.2" + unique-filename "^1.1.1" + +call-bind@^1.0.0, call-bind@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camel-case@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a" + integrity sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw== + dependencies: + pascal-case "^3.1.2" + tslib "^2.0.3" + +camelcase-keys@^6.2.2: + version "6.2.2" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0" + integrity sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg== + dependencies: + camelcase "^5.3.1" + map-obj "^4.0.0" + quick-lru "^4.0.1" + +camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +camelcase@^6.2.0, camelcase@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +caniuse-lite@^1.0.30001317: + version "1.0.30001319" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001319.tgz#eb4da4eb3ecdd409f7ba1907820061d56096e88f" + integrity sha512-xjlIAFHucBRSMUo1kb5D4LYgcN1M45qdKP++lhqowDpwJwGkpIRTt5qQqnhxjj1vHcI7nrJxWhCC1ATrCEBTcw== + +capital-case@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/capital-case/-/capital-case-1.0.4.tgz#9d130292353c9249f6b00fa5852bee38a717e669" + integrity sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + upper-case-first "^2.0.2" + +case@1.6.3, case@^1.6.3: + version "1.6.3" + resolved "https://registry.yarnpkg.com/case/-/case-1.6.3.tgz#0a4386e3e9825351ca2e6216c60467ff5f1ea1c9" + integrity sha512-mzDSXIPaFwVDvZAHqZ9VlbyF4yyXRuX6IvB06WvPYkqJVO24kX1PPhv9bfpKNFZyxYFmmgo03HUiD8iklmJYRQ== + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= + +cdk-assets@2.16.0: + version "2.16.0" + resolved "https://registry.yarnpkg.com/cdk-assets/-/cdk-assets-2.16.0.tgz#d375a036696b63fa1f0c0914284b73c5b7f880c0" + integrity sha512-1auNvrf42wo5DWTkOzSXJtjGnxQyrizPZaFzHqEtqawHNTVrSw8IMXwNqyiB+1rnXVKXDY/WyVJa2ysTKEzOYw== + dependencies: + "@aws-cdk/cloud-assembly-schema" "2.16.0" + "@aws-cdk/cx-api" "2.16.0" + archiver "^5.3.0" + aws-sdk "^2.848.0" + glob "^7.2.0" + mime "^2.6.0" + yargs "^16.2.0" + +cdk-nag@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/cdk-nag/-/cdk-nag-2.5.2.tgz#b7a03dde19abb92eee5b001f7e5b0a638cfa70ae" + integrity sha512-tIqIunchUtqP8a7efOtgutyfnJzimpTRtMfE8+4CcEm6Ze4CisJ+4qRPoVH1rmOxqHMNvjZh5TSTxrhRCEBwjw== + +chalk@^2.0.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4, chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +change-case@4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/change-case/-/change-case-4.1.2.tgz#fedfc5f136045e2398c0410ee441f95704641e12" + integrity sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A== + dependencies: + camel-case "^4.1.2" + capital-case "^1.0.4" + constant-case "^3.0.4" + dot-case "^3.0.4" + header-case "^2.0.4" + no-case "^3.0.4" + param-case "^3.0.4" + pascal-case "^3.1.2" + path-case "^3.0.4" + sentence-case "^3.0.4" + snake-case "^3.0.4" + tslib "^2.0.3" + +char-regex@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" + integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== + +chardet@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" + integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== + +charenc@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" + integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= + +chokidar@3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chownr@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" + integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== + +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + +ci-info@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" + integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== + +ci-info@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.3.0.tgz#b4ed1fb6818dea4803a55c623041f9165d2066b2" + integrity sha512-riT/3vI5YpVH6/qomlDnJow6TBee2PBKSEpx3O32EGPYbWGIRsIlGRms3Sm74wYE1JMo8RnO04Hb12+v1J5ICw== + +cjs-module-lexer@^1.0.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40" + integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== + +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + +cli-truncate@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" + integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== + dependencies: + slice-ansi "^3.0.0" + string-width "^4.2.0" + +cli-truncate@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-3.1.0.tgz#3f23ab12535e3d73e839bb43e73c9de487db1389" + integrity sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA== + dependencies: + slice-ansi "^5.0.0" + string-width "^5.0.0" + +cli-width@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" + integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== + dependencies: + is-plain-object "^2.0.4" + kind-of "^6.0.2" + shallow-clone "^3.0.0" + +clone@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" + integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= + +clone@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" + integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18= + +cmd-shim@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-4.1.0.tgz#b3a904a6743e9fede4148c6f3800bf2a08135bdd" + integrity sha512-lb9L7EM4I/ZRVuljLPEtUJOP+xiQVknZ4ZMpMgEp4JzNldPb27HU03hi6K1/6CoIuit/Zm/LQXySErFeXxDprw== + dependencies: + mkdirp-infer-owner "^2.0.0" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= + +codemaker@^1.47.0: + version "1.55.1" + resolved "https://registry.yarnpkg.com/codemaker/-/codemaker-1.55.1.tgz#ca2ae0e3c7a3e9909740a603648e68d4a20e1a76" + integrity sha512-W0MZSFgqfr9mgKbYLHsTNYTMKiXQE9hDHs6qke5dC5S9ZlFgcWG2zdpznknwvPLDDuWP8Z5QL71MjAM21hEPOg== + dependencies: + camelcase "^6.3.0" + decamelize "^5.0.1" + fs-extra "^9.1.0" + +collect-v8-coverage@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59" + integrity sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg== + +color-convert@^1.9.0, color-convert@^1.9.3: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +color-name@^1.0.0, color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-string@^1.6.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.0.tgz#63b6ebd1bec11999d1df3a79a7569451ac2be8aa" + integrity sha512-9Mrz2AQLefkH1UvASKj6v6hj/7eWgjnT/cVsR8CumieLoT+g900exWeNogqtweI8dxloXN9BDQTYro1oWu/5CQ== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + +color@^3.1.3: + version "3.2.1" + resolved "https://registry.yarnpkg.com/color/-/color-3.2.1.tgz#3544dc198caf4490c3ecc9a790b54fe9ff45e164" + integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA== + dependencies: + color-convert "^1.9.3" + color-string "^1.6.0" + +colorette@^2.0.16: + version "2.0.16" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.16.tgz#713b9af84fdb000139f04546bd4a93f62a5085da" + integrity sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g== + +colors@1.4.0, colors@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" + integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== + +colorspace@1.1.x: + version "1.1.4" + resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.1.4.tgz#8d442d1186152f60453bf8070cd66eb364e59243" + integrity sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w== + dependencies: + color "^3.1.3" + text-hex "1.0.x" + +columnify@^1.5.4: + version "1.6.0" + resolved "https://registry.yarnpkg.com/columnify/-/columnify-1.6.0.tgz#6989531713c9008bb29735e61e37acf5bd553cf3" + integrity sha512-lomjuFZKfM6MSAnV9aCZC9sc0qGbmZdfygNv+nCpqVkSKdCxCklLtd16O0EILGkImHw9ZpHkAnHaB+8Zxq5W6Q== + dependencies: + strip-ansi "^6.0.1" + wcwidth "^1.0.0" + +combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.2.tgz#6837c3fb677ad9933d1cfba42dd14d5117d6b39e" + integrity sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow== + +commander@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" + integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== + +commonmark@^0.30.0: + version "0.30.0" + resolved "https://registry.yarnpkg.com/commonmark/-/commonmark-0.30.0.tgz#38811dc7bbf0f59d277ae09054d4d73a332f2e45" + integrity sha512-j1yoUo4gxPND1JWV9xj5ELih0yMv1iCWDG6eEQIPLSWLxzCXiFoyS7kvB+WwU+tZMf4snwJMMtaubV0laFpiBA== + dependencies: + entities "~2.0" + mdurl "~1.0.1" + minimist ">=1.2.2" + string.prototype.repeat "^0.2.0" + +compare-func@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/compare-func/-/compare-func-2.0.0.tgz#fb65e75edbddfd2e568554e8b5b05fff7a51fcb3" + integrity sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA== + dependencies: + array-ify "^1.0.0" + dot-prop "^5.1.0" + +compress-commons@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-4.1.1.tgz#df2a09a7ed17447642bad10a85cc9a19e5c42a7d" + integrity sha512-QLdDLCKNV2dtoTorqgxngQCMA+gWXkM/Nwu7FpeBhk/RdkzimqC3jueb/FDmaZeXh+uby1jkBqE3xArsLBE5wQ== + dependencies: + buffer-crc32 "^0.2.13" + crc32-stream "^4.0.2" + normalize-path "^3.0.0" + readable-stream "^3.6.0" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +concat-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-2.0.0.tgz#414cf5af790a48c60ab9be4527d56d5e41133cb1" + integrity sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.0.2" + typedarray "^0.0.6" + +config-chain@^1.1.12: + version "1.1.13" + resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.13.tgz#fad0795aa6a6cdaff9ed1b68e9dff94372c232f4" + integrity sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ== + dependencies: + ini "^1.3.4" + proto-list "~1.2.1" + +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= + +constant-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/constant-case/-/constant-case-3.0.4.tgz#3b84a9aeaf4cf31ec45e6bf5de91bdfb0589faf1" + integrity sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + upper-case "^2.0.2" + +constructs@10.0.12: + version "10.0.12" + resolved "https://registry.yarnpkg.com/constructs/-/constructs-10.0.12.tgz#64f314d87f7cdf6da127c9eb0bf6a3d990989c4a" + integrity sha512-wVQcQgwwK7b//7yI54/3hundmXAw7RBpuy5f6yIBFNceJr8feTK6Cs2I2f3+gp3/ikszzTouLup9AzxioEEXPQ== + +conventional-changelog-angular@^5.0.12: + version "5.0.13" + resolved "https://registry.yarnpkg.com/conventional-changelog-angular/-/conventional-changelog-angular-5.0.13.tgz#896885d63b914a70d4934b59d2fe7bde1832b28c" + integrity sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA== + dependencies: + compare-func "^2.0.0" + q "^1.5.1" + +conventional-changelog-core@^4.2.2: + version "4.2.4" + resolved "https://registry.yarnpkg.com/conventional-changelog-core/-/conventional-changelog-core-4.2.4.tgz#e50d047e8ebacf63fac3dc67bf918177001e1e9f" + integrity sha512-gDVS+zVJHE2v4SLc6B0sLsPiloR0ygU7HaDW14aNJE1v4SlqJPILPl/aJC7YdtRE4CybBf8gDwObBvKha8Xlyg== + dependencies: + add-stream "^1.0.0" + conventional-changelog-writer "^5.0.0" + conventional-commits-parser "^3.2.0" + dateformat "^3.0.0" + get-pkg-repo "^4.0.0" + git-raw-commits "^2.0.8" + git-remote-origin-url "^2.0.0" + git-semver-tags "^4.1.1" + lodash "^4.17.15" + normalize-package-data "^3.0.0" + q "^1.5.1" + read-pkg "^3.0.0" + read-pkg-up "^3.0.0" + through2 "^4.0.0" + +conventional-changelog-preset-loader@^2.3.4: + version "2.3.4" + resolved "https://registry.yarnpkg.com/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.3.4.tgz#14a855abbffd59027fd602581f1f34d9862ea44c" + integrity sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g== + +conventional-changelog-writer@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/conventional-changelog-writer/-/conventional-changelog-writer-5.0.1.tgz#e0757072f045fe03d91da6343c843029e702f359" + integrity sha512-5WsuKUfxW7suLblAbFnxAcrvf6r+0b7GvNaWUwUIk0bXMnENP/PEieGKVUQrjPqwPT4o3EPAASBXiY6iHooLOQ== + dependencies: + conventional-commits-filter "^2.0.7" + dateformat "^3.0.0" + handlebars "^4.7.7" + json-stringify-safe "^5.0.1" + lodash "^4.17.15" + meow "^8.0.0" + semver "^6.0.0" + split "^1.0.0" + through2 "^4.0.0" + +conventional-commits-filter@^2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/conventional-commits-filter/-/conventional-commits-filter-2.0.7.tgz#f8d9b4f182fce00c9af7139da49365b136c8a0b3" + integrity sha512-ASS9SamOP4TbCClsRHxIHXRfcGCnIoQqkvAzCSbZzTFLfcTqJVugB0agRgsEELsqaeWgsXv513eS116wnlSSPA== + dependencies: + lodash.ismatch "^4.4.0" + modify-values "^1.0.0" + +conventional-commits-parser@^3.2.0: + version "3.2.4" + resolved "https://registry.yarnpkg.com/conventional-commits-parser/-/conventional-commits-parser-3.2.4.tgz#a7d3b77758a202a9b2293d2112a8d8052c740972" + integrity sha512-nK7sAtfi+QXbxHCYfhpZsfRtaitZLIA6889kFIouLvz6repszQDgxBu7wf2WbU+Dco7sAnNCJYERCwt54WPC2Q== + dependencies: + JSONStream "^1.0.4" + is-text-path "^1.0.1" + lodash "^4.17.15" + meow "^8.0.0" + split2 "^3.0.0" + through2 "^4.0.0" + +conventional-recommended-bump@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/conventional-recommended-bump/-/conventional-recommended-bump-6.1.0.tgz#cfa623285d1de554012f2ffde70d9c8a22231f55" + integrity sha512-uiApbSiNGM/kkdL9GTOLAqC4hbptObFo4wW2QRyHsKciGAfQuLU1ShZ1BIVI/+K2BE/W1AWYQMCXAsv4dyKPaw== + dependencies: + concat-stream "^2.0.0" + conventional-changelog-preset-loader "^2.3.4" + conventional-commits-filter "^2.0.7" + conventional-commits-parser "^3.2.0" + git-raw-commits "^2.0.8" + git-semver-tags "^4.1.1" + meow "^8.0.0" + q "^1.5.1" + +convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" + integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA== + dependencies: + safe-buffer "~5.1.1" + +core-util-is@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cosmiconfig@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.1.tgz#714d756522cace867867ccb4474c5d01bbae5d6d" + integrity sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + +crc-32@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.1.tgz#436d2bcaad27bcb6bd073a2587139d3024a16460" + integrity sha512-Dn/xm/1vFFgs3nfrpEVScHoIslO9NZRITWGz/1E/St6u4xw99vfZzVkW0OSnzx2h9egej9xwMCEut6sqwokM/w== + dependencies: + exit-on-epipe "~1.0.1" + printj "~1.3.1" + +crc32-stream@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-4.0.2.tgz#c922ad22b38395abe9d3870f02fa8134ed709007" + integrity sha512-DxFZ/Hk473b/muq1VJ///PMNLj0ZMnzye9thBpmjpJKCc5eMgB95aK8zCGrGfQ90cWo561Te6HK9D+j4KPdM6w== + dependencies: + crc-32 "^1.2.0" + readable-stream "^3.4.0" + +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + +cross-spawn@^7.0.2, cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +crypt@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" + integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs= + +crypto-random-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-4.0.0.tgz#5a3cc53d7dd86183df5da0312816ceeeb5bb1fc2" + integrity sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA== + dependencies: + type-fest "^1.0.1" + +cssom@^0.4.4: + version "0.4.4" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" + integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw== + +cssom@~0.3.6: + version "0.3.8" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" + integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== + +cssstyle@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.3.0.tgz#ff665a0ddbdc31864b09647f34163443d90b0852" + integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A== + dependencies: + cssom "~0.3.6" + +dargs@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/dargs/-/dargs-7.0.0.tgz#04015c41de0bcb69ec84050f3d9be0caf8d6d5cc" + integrity sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg== + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= + dependencies: + assert-plus "^1.0.0" + +data-uri-to-buffer@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz#594b8973938c5bc2c33046535785341abc4f3636" + integrity sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og== + +data-urls@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b" + integrity sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ== + dependencies: + abab "^2.0.3" + whatwg-mimetype "^2.3.0" + whatwg-url "^8.0.0" + +date-format@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/date-format/-/date-format-4.0.6.tgz#f6138b8f17968df9815b3d101fc06b0523f066c5" + integrity sha512-B9vvg5rHuQ8cbUXE/RMWMyX2YA5TecT3jKF5fLtGNlzPlU7zblSPmAm2OImDbWL+LDOQ6pUm+4LOFz+ywS41Zw== + +dateformat@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" + integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== + +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +debug@^2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^3.2.7: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + +debuglog@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" + integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI= + +decamelize-keys@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9" + integrity sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk= + dependencies: + decamelize "^1.1.0" + map-obj "^1.0.0" + +decamelize@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= + +decamelize@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-5.0.1.tgz#db11a92e58c741ef339fb0a2868d8a06a9a7b1e9" + integrity sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA== + +decimal.js@^10.2.1: + version "10.3.1" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783" + integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ== + +decode-uri-component@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" + integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= + +dedent@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" + integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= + +deep-equal@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.0.5.tgz#55cd2fe326d83f9cbf7261ef0e060b3f724c5cb9" + integrity sha512-nPiRgmbAtm1a3JsnLCf6/SLfXcjyN5v8L1TXzdCmHrXJ4hx+gW/w1YCcn7z8gJtSiDArZCgYtbao3QqLm/N1Sw== + dependencies: + call-bind "^1.0.0" + es-get-iterator "^1.1.1" + get-intrinsic "^1.0.1" + is-arguments "^1.0.4" + is-date-object "^1.0.2" + is-regex "^1.1.1" + isarray "^2.0.5" + object-is "^1.1.4" + object-keys "^1.1.1" + object.assign "^4.1.2" + regexp.prototype.flags "^1.3.0" + side-channel "^1.0.3" + which-boxed-primitive "^1.0.1" + which-collection "^1.0.1" + which-typed-array "^1.1.2" + +deep-is@^0.1.3, deep-is@~0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +deepmerge@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" + integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== + +defaults@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d" + integrity sha1-xlYFHpgX2f8I7YgUd/P+QBnz730= + dependencies: + clone "^1.0.2" + +define-properties@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" + integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== + dependencies: + object-keys "^1.0.12" + +degenerator@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/degenerator/-/degenerator-3.0.2.tgz#6a61fcc42a702d6e50ff6023fe17bff435f68235" + integrity sha512-c0mef3SNQo56t6urUU6tdQAs+ThoD0o9B9MJ8HEt7NQcGEILCRFqQb7ZbP9JAv+QF1Ky5plydhMR/IrqWDm+TQ== + dependencies: + ast-types "^0.13.2" + escodegen "^1.8.1" + esprima "^4.0.0" + vm2 "^3.9.8" + +del@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/del/-/del-6.0.0.tgz#0b40d0332cea743f1614f818be4feb717714c952" + integrity sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ== + dependencies: + globby "^11.0.1" + graceful-fs "^4.2.4" + is-glob "^4.0.1" + is-path-cwd "^2.2.0" + is-path-inside "^3.0.2" + p-map "^4.0.0" + rimraf "^3.0.2" + slash "^3.0.0" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +depd@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + +deprecation@^2.0.0, deprecation@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" + integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== + +detect-indent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d" + integrity sha1-OHHMCmoALow+Wzz38zYmRnXwa50= + +detect-indent@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" + integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== + +detect-newline@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" + integrity sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I= + +detect-newline@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" + integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== + +dezalgo@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456" + integrity sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY= + dependencies: + asap "^2.0.0" + wrappy "1" + +diff-sequences@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327" + integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ== + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +diff@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" + integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +doctrine@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" + integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== + dependencies: + esutils "^2.0.2" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +domexception@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/domexception/-/domexception-2.0.1.tgz#fb44aefba793e1574b0af6aed2801d057529f304" + integrity sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg== + dependencies: + webidl-conversions "^5.0.0" + +dot-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" + integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + +dot-prop@^5.1.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" + integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q== + dependencies: + is-obj "^2.0.0" + +dot-prop@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-6.0.1.tgz#fc26b3cf142b9e59b74dbd39ed66ce620c681083" + integrity sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA== + dependencies: + is-obj "^2.0.0" + +duplexer@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" + integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== + +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +electron-to-chromium@^1.4.84: + version "1.4.89" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.89.tgz#33c06592812a17a7131873f4596579084ce33ff8" + integrity sha512-z1Axg0Fu54fse8wN4fd+GAINdU5mJmLtcl6bqIcYyzNVGONcfHAeeJi88KYMQVKalhXlYuVPzKkFIU5VD0raUw== + +emittery@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.8.1.tgz#bb23cc86d03b30aa75a7f734819dee2e1ba70860" + integrity sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + +enabled@2.0.x: + version "2.0.0" + resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2" + integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== + +encoding@^0.1.12: + version "0.1.13" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" + integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== + dependencies: + iconv-lite "^0.6.2" + +end-of-stream@^1.4.1: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +enquirer@^2.3.5, enquirer@^2.3.6: + version "2.3.6" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" + integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== + dependencies: + ansi-colors "^4.1.1" + +entities@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" + integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== + +entities@~2.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.3.tgz#5c487e5742ab93c15abb5da22759b8590ec03b7f" + integrity sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ== + +env-paths@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" + integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== + +envinfo@^7.7.4: + version "7.8.1" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475" + integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw== + +err-code@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" + integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +es-abstract@^1.18.5, es-abstract@^1.19.0, es-abstract@^1.19.1: + version "1.19.1" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.1.tgz#d4885796876916959de78edaa0df456627115ec3" + integrity sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w== + dependencies: + call-bind "^1.0.2" + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + get-intrinsic "^1.1.1" + get-symbol-description "^1.0.0" + has "^1.0.3" + has-symbols "^1.0.2" + internal-slot "^1.0.3" + is-callable "^1.2.4" + is-negative-zero "^2.0.1" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.1" + is-string "^1.0.7" + is-weakref "^1.0.1" + object-inspect "^1.11.0" + object-keys "^1.1.1" + object.assign "^4.1.2" + string.prototype.trimend "^1.0.4" + string.prototype.trimstart "^1.0.4" + unbox-primitive "^1.0.1" + +es-get-iterator@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.2.tgz#9234c54aba713486d7ebde0220864af5e2b283f7" + integrity sha512-+DTO8GYwbMCwbywjimwZMHp8AuYXOS2JZFWoi2AlPOS3ebnII9w/NLpNZtA7A0YLaVDw+O7KFCeoIV7OPvM7hQ== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.0" + has-symbols "^1.0.1" + is-arguments "^1.1.0" + is-map "^2.0.2" + is-set "^2.0.2" + is-string "^1.0.5" + isarray "^2.0.5" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +esbuild-android-arm64@0.14.2: + version "0.14.2" + resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.2.tgz#256b7cf2f9d382a2a92a4ff4e13187587c9b7c6a" + integrity sha512-hEixaKMN3XXCkoe+0WcexO4CcBVU5DCSUT+7P8JZiWZCbAjSkc9b6Yz2X5DSfQmRCtI/cQRU6TfMYrMQ5NBfdw== + +esbuild-darwin-64@0.14.2: + version "0.14.2" + resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.2.tgz#891a59ce6bc3aded0265f982469b3eb9571b92f8" + integrity sha512-Uq8t0cbJQkxkQdbUfOl2wZqZ/AtLZjvJulR1HHnc96UgyzG9YlCLSDMiqjM+NANEy7/zzvwKJsy3iNC9wwqLJA== + +esbuild-darwin-arm64@0.14.2: + version "0.14.2" + resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.2.tgz#ab834fffa9c612b2901ca1e77e4695d4d8aa63a2" + integrity sha512-619MSa17sr7YCIrUj88KzQu2ESA4jKYtIYfLU/smX6qNgxQt3Y/gzM4s6sgJ4fPQzirvmXgcHv1ZNQAs/Xh48A== + +esbuild-freebsd-64@0.14.2: + version "0.14.2" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.2.tgz#f7fc87a83f02de27d5a48472571efa1a432ae86d" + integrity sha512-aP6FE/ZsChZpUV6F3HE3x1Pz0paoYXycJ7oLt06g0G9dhJKknPawXCqQg/WMyD+ldCEZfo7F1kavenPdIT/SGQ== + +esbuild-freebsd-arm64@0.14.2: + version "0.14.2" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.2.tgz#bc8758420431106751f3180293cac0b5bc4ce2ee" + integrity sha512-LSm98WTb1QIhyS83+Po0KTpZNdd2XpVpI9ua5rLWqKWbKeNRFwOsjeiuwBaRNc+O32s9oC2ZMefETxHBV6VNkQ== + +esbuild-linux-32@0.14.2: + version "0.14.2" + resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.2.tgz#0cc2dcd816d6d66e255bc7aeac139b1d04246812" + integrity sha512-8VxnNEyeUbiGflTKcuVc5JEPTqXfsx2O6ABwUbfS1Hp26lYPRPC7pKQK5Dxa0MBejGc50jy7YZae3EGQUQ8EkQ== + +esbuild-linux-64@0.14.2: + version "0.14.2" + resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.2.tgz#c790f739aa75b15c153609ea3457153fbe4db93d" + integrity sha512-4bzMS2dNxOJoFIiHId4w+tqQzdnsch71JJV1qZnbnErSFWcR9lRgpSqWnTTFtv6XM+MvltRzSXC5wQ7AEBY6Hg== + +esbuild-linux-arm64@0.14.2: + version "0.14.2" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.2.tgz#96858a1f89ad30274dec780d0e3dd8b5691c6b0c" + integrity sha512-RlIVp0RwJrdtasDF1vTFueLYZ8WuFzxoQ1OoRFZOTyJHCGCNgh7xJIC34gd7B7+RT0CzLBB4LcM5n0LS+hIoww== + +esbuild-linux-arm@0.14.2: + version "0.14.2" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.2.tgz#03e193225afa9b1215d2ec6efe8edf0c03eeed6f" + integrity sha512-PaylahvMHhH8YMfJPMKEqi64qA0Su+d4FNfHKvlKes/2dUe4QxgbwXT9oLVgy8iJdcFMrO7By4R8fS8S0p8aVQ== + +esbuild-linux-mips64le@0.14.2: + version "0.14.2" + resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.2.tgz#972f218d2cb5125237376d40ad60a6e5356a782c" + integrity sha512-Fdwrq2roFnO5oetIiUQQueZ3+5soCxBSJswg3MvYaXDomj47BN6oAWMZgLrFh1oVrtWrxSDLCJBenYdbm2s+qQ== + +esbuild-linux-ppc64le@0.14.2: + version "0.14.2" + resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.2.tgz#20b71622ac09142b0e523f633af0829def7fed6b" + integrity sha512-vxptskw8JfCDD9QqpRO0XnsM1osuWeRjPaXX1TwdveLogYsbdFtcuiuK/4FxGiNMUr1ojtnCS2rMPbY8puc5NA== + +esbuild-netbsd-64@0.14.2: + version "0.14.2" + resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.2.tgz#dbd6a25117902ef67aa11d8779dd9c6bca7fbe82" + integrity sha512-I8+LzYK5iSNpspS9eCV9sW67Rj8FgMHimGri4mKiGAmN0pNfx+hFX146rYtzGtewuxKtTsPywWteHx+hPRLDsw== + +esbuild-openbsd-64@0.14.2: + version "0.14.2" + resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.2.tgz#3c5f199eed459b2f88865548394c0b77383d9ca4" + integrity sha512-120HgMe9elidWUvM2E6mMf0csrGwx8sYDqUIJugyMy1oHm+/nT08bTAVXuwYG/rkMIqsEO9AlMxuYnwR6En/3Q== + +esbuild-sunos-64@0.14.2: + version "0.14.2" + resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.2.tgz#900a681db6b76c6a7f60fc28d2bfe5b11698641c" + integrity sha512-Q3xcf9Uyfra9UuCFxoLixVvdigo0daZaKJ97TL2KNA4bxRUPK18wwGUk3AxvgDQZpRmg82w9PnkaNYo7a+24ow== + +esbuild-windows-32@0.14.2: + version "0.14.2" + resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.2.tgz#61e0ba5bd95b277a55d2b997ac4c04dfe2559220" + integrity sha512-TW7O49tPsrq+N1sW8mb3m24j/iDGa4xzAZH4wHWwoIzgtZAYPKC0hpIhufRRG/LA30bdMChO9pjJZ5mtcybtBQ== + +esbuild-windows-64@0.14.2: + version "0.14.2" + resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.2.tgz#6ab59ef721ff75c682a1c8ae0570dabb637abddb" + integrity sha512-Rym6ViMNmi1E2QuQMWy0AFAfdY0wGwZD73BnzlsQBX5hZBuy/L+Speh7ucUZ16gwsrMM9v86icZUDrSN/lNBKg== + +esbuild-windows-arm64@0.14.2: + version "0.14.2" + resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.2.tgz#aca2a4f83d2f0d1592ad4be832ed0045fc888cda" + integrity sha512-ZrLbhr0vX5Em/P1faMnHucjVVWPS+m3tktAtz93WkMZLmbRJevhiW1y4CbulBd2z0MEdXZ6emDa1zFHq5O5bSA== + +esbuild@0.14.2: + version "0.14.2" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.2.tgz#9c1e1a652549cc33e44885eea42ea2cc6267edc2" + integrity sha512-l076A6o/PIgcyM24s0dWmDI/b8RQf41uWoJu9I0M71CtW/YSw5T5NUeXxs5lo2tFQD+O4CW4nBHJXx3OY5NpXg== + optionalDependencies: + esbuild-android-arm64 "0.14.2" + esbuild-darwin-64 "0.14.2" + esbuild-darwin-arm64 "0.14.2" + esbuild-freebsd-64 "0.14.2" + esbuild-freebsd-arm64 "0.14.2" + esbuild-linux-32 "0.14.2" + esbuild-linux-64 "0.14.2" + esbuild-linux-arm "0.14.2" + esbuild-linux-arm64 "0.14.2" + esbuild-linux-mips64le "0.14.2" + esbuild-linux-ppc64le "0.14.2" + esbuild-netbsd-64 "0.14.2" + esbuild-openbsd-64 "0.14.2" + esbuild-sunos-64 "0.14.2" + esbuild-windows-32 "0.14.2" + esbuild-windows-64 "0.14.2" + esbuild-windows-arm64 "0.14.2" + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +escodegen@^1.8.1: + version "1.14.3" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" + integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw== + dependencies: + esprima "^4.0.1" + estraverse "^4.2.0" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.6.1" + +escodegen@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd" + integrity sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw== + dependencies: + esprima "^4.0.1" + estraverse "^5.2.0" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.6.1" + +eslint-config-prettier@8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz#f7471b20b6fe8a9a9254cc684454202886a2dd7a" + integrity sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew== + +eslint-config-standard@16.0.3: + version "16.0.3" + resolved "https://registry.yarnpkg.com/eslint-config-standard/-/eslint-config-standard-16.0.3.tgz#6c8761e544e96c531ff92642eeb87842b8488516" + integrity sha512-x4fmJL5hGqNJKGHSjnLdgA6U6h1YW/G2dW9fA+cyVur4SK6lyue8+UgNKWlZtUDTXvgKDD/Oa3GQjmB5kjtVvg== + +eslint-import-resolver-node@0.3.6, eslint-import-resolver-node@^0.3.6: + version "0.3.6" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz#4048b958395da89668252001dbd9eca6b83bacbd" + integrity sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw== + dependencies: + debug "^3.2.7" + resolve "^1.20.0" + +eslint-import-resolver-typescript@2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-2.5.0.tgz#07661966b272d14ba97f597b51e1a588f9722f0a" + integrity sha512-qZ6e5CFr+I7K4VVhQu3M/9xGv9/YmwsEXrsm3nimw8vWaVHRDrQRp26BgCypTxBp3vUp4o5aVEJRiy0F2DFddQ== + dependencies: + debug "^4.3.1" + glob "^7.1.7" + is-glob "^4.0.1" + resolve "^1.20.0" + tsconfig-paths "^3.9.0" + +eslint-module-utils@^2.7.1: + version "2.7.3" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.3.tgz#ad7e3a10552fdd0642e1e55292781bd6e34876ee" + integrity sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ== + dependencies: + debug "^3.2.7" + find-up "^2.1.0" + +eslint-plugin-es@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz#75a7cdfdccddc0589934aeeb384175f221c57893" + integrity sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ== + dependencies: + eslint-utils "^2.0.0" + regexpp "^3.0.0" + +eslint-plugin-import@2.25.3: + version "2.25.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.25.3.tgz#a554b5f66e08fb4f6dc99221866e57cfff824766" + integrity sha512-RzAVbby+72IB3iOEL8clzPLzL3wpDrlwjsTBAQXgyp5SeTqqY+0bFubwuo+y/HLhNZcXV4XqTBO4LGsfyHIDXg== + dependencies: + array-includes "^3.1.4" + array.prototype.flat "^1.2.5" + debug "^2.6.9" + doctrine "^2.1.0" + eslint-import-resolver-node "^0.3.6" + eslint-module-utils "^2.7.1" + has "^1.0.3" + is-core-module "^2.8.0" + is-glob "^4.0.3" + minimatch "^3.0.4" + object.values "^1.1.5" + resolve "^1.20.0" + tsconfig-paths "^3.11.0" + +eslint-plugin-jest@25.3.0: + version "25.3.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-25.3.0.tgz#6c04bbf13624a75684a05391a825b58e2e291950" + integrity sha512-79WQtuBsTN1S8Y9+7euBYwxIOia/k7ykkl9OCBHL3xuww5ecursHy/D8GCIlvzHVWv85gOkS5Kv6Sh7RxOgK1Q== + dependencies: + "@typescript-eslint/experimental-utils" "^5.0.0" + +eslint-plugin-license-header@0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-license-header/-/eslint-plugin-license-header-0.2.1.tgz#01941e3074e0bc4f110220ba20244436a5f84e45" + integrity sha512-l+fiPajCJztDzIHJ8BA6AFy/0oDhI8ZovytzRp1dY49tiW7BP+1yKp2cv7SPlTAkTtUH1MFEYspPS4zxVhQ3QA== + dependencies: + requireindex "^1.2.0" + +eslint-plugin-node@11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz#c95544416ee4ada26740a30474eefc5402dc671d" + integrity sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g== + dependencies: + eslint-plugin-es "^3.0.0" + eslint-utils "^2.0.0" + ignore "^5.1.1" + minimatch "^3.0.4" + resolve "^1.10.1" + semver "^6.1.0" + +eslint-plugin-prettier@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-4.0.0.tgz#8b99d1e4b8b24a762472b4567992023619cb98e0" + integrity sha512-98MqmCJ7vJodoQK359bqQWaxOE0CS8paAz/GgjaZLyex4TTk3g9HugoO89EqWCrFiOqn9EVvcoo7gZzONCWVwQ== + dependencies: + prettier-linter-helpers "^1.0.0" + +eslint-scope@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +eslint-scope@^7.1.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.1.tgz#fff34894c2f65e5226d3041ac480b4513a163642" + integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-utils@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" + integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== + dependencies: + eslint-visitor-keys "^1.1.0" + +eslint-utils@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" + integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== + dependencies: + eslint-visitor-keys "^2.0.0" + +eslint-visitor-keys@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" + integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== + +eslint-visitor-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" + integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== + +eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.1.0, eslint-visitor-keys@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" + integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== + +eslint@8.4.1: + version "8.4.1" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.4.1.tgz#d6531bbf3e598dffd7c0c7d35ec52a0b30fdfa2d" + integrity sha512-TxU/p7LB1KxQ6+7aztTnO7K0i+h0tDi81YRY9VzB6Id71kNz+fFYnf5HD5UOQmxkzcoa0TlVZf9dpMtUv0GpWg== + dependencies: + "@eslint/eslintrc" "^1.0.5" + "@humanwhocodes/config-array" "^0.9.2" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + enquirer "^2.3.5" + escape-string-regexp "^4.0.0" + eslint-scope "^7.1.0" + eslint-utils "^3.0.0" + eslint-visitor-keys "^3.1.0" + espree "^9.2.0" + esquery "^1.4.0" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + functional-red-black-tree "^1.0.1" + glob-parent "^6.0.1" + globals "^13.6.0" + ignore "^4.0.6" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + js-yaml "^4.1.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.0.4" + natural-compare "^1.4.0" + optionator "^0.9.1" + progress "^2.0.0" + regexpp "^3.2.0" + semver "^7.2.1" + strip-ansi "^6.0.1" + strip-json-comments "^3.1.0" + text-table "^0.2.0" + v8-compile-cache "^2.0.3" + +espree@^9.2.0, espree@^9.3.1: + version "9.3.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.1.tgz#8793b4bc27ea4c778c19908e0719e7b8f4115bcd" + integrity sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ== + dependencies: + acorn "^8.7.0" + acorn-jsx "^5.3.1" + eslint-visitor-keys "^3.3.0" + +esprima@^4.0.0, esprima@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esquery@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" + integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1, estraverse@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.1.0, estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +eventemitter3@^4.0.4: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + +events@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" + integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= + +execa@^5.0.0, execa@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + +exit-on-epipe@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz#0bdd92e87d5285d267daa8171d0eb06159689692" + integrity sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw== + +exit@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + integrity sha1-BjJjj42HfMghB9MKD/8aF8uhzQw= + +expect@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/expect/-/expect-27.5.1.tgz#83ce59f1e5bdf5f9d2b94b61d2050db48f3fef74" + integrity sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw== + dependencies: + "@jest/types" "^27.5.1" + jest-get-type "^27.5.1" + jest-matcher-utils "^27.5.1" + jest-message-util "^27.5.1" + +exponential-backoff@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.0.tgz#9409c7e579131f8bd4b32d7d8094a911040f2e68" + integrity sha512-oBuz5SYz5zzyuHINoe9ooePwSu0xApKWgeNzok4hZ5YKXFh9zrQBEM15CXqoZkJJPuI2ArvqjPQd8UKJA753XA== + +extend@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +external-editor@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" + integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== + dependencies: + chardet "^0.7.0" + iconv-lite "^0.4.24" + tmp "^0.0.33" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= + +extsprintf@^1.2.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07" + integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-diff@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" + integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== + +fast-glob@^3.2.11, fast-glob@^3.2.9: + version "3.2.11" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9" + integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + +fast-xml-parser@3.19.0: + version "3.19.0" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-3.19.0.tgz#cb637ec3f3999f51406dd8ff0e6fc4d83e520d01" + integrity sha512-4pXwmBplsCPv8FOY1WRakF970TjNGnGnfbOnLqjlYvMiF1SR3yOHyxMR/YCXpPTOspNF5gwudqktIP4VsWkvBg== + +fastq@^1.6.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" + integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw== + dependencies: + reusify "^1.0.4" + +fb-watchman@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz#fc84fb39d2709cf3ff6d743706157bb5708a8a85" + integrity sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg== + dependencies: + bser "2.1.1" + +fecha@^4.2.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.1.tgz#0a83ad8f86ef62a091e22bb5a039cd03d23eecce" + integrity sha512-MMMQ0ludy/nBs1/o0zVOiKTpG7qMbonKUzjJgQFEuvq6INZ1OraKPRAWkBq5vlKLOUMpmNYG1JoN3oDPUQ9m3Q== + +figures@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== + dependencies: + escape-string-regexp "^1.0.5" + +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + +file-uri-to-path@2: + version "2.0.0" + resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-2.0.0.tgz#7b415aeba227d575851e0a5b0c640d7656403fba" + integrity sha512-hjPFI8oE/2iQPVe4gbrJ73Pp+Xfub2+WI2LlXDbsaJBwT5wuMh35WNWVYYTpnz895shtwfyutMFLFywpQAFdLg== + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +filter-obj@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-1.1.0.tgz#9b311112bc6c6127a16e016c6c5d7f19e0805c5b" + integrity sha1-mzERErxsYSehbgFsbF1/GeCAXFs= + +find-up@^2.0.0, find-up@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= + dependencies: + locate-path "^2.0.0" + +find-up@^4.0.0, find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +flat-cache@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" + integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + dependencies: + flatted "^3.1.0" + rimraf "^3.0.2" + +flatted@^3.1.0, flatted@^3.2.5: + version "3.2.5" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3" + integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg== + +fn.name@1.x.x: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc" + integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== + +foreach@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" + integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k= + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= + +form-data@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" + integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +fp-ts@2.11.5: + version "2.11.5" + resolved "https://registry.yarnpkg.com/fp-ts/-/fp-ts-2.11.5.tgz#97cceb26655b1452d7088d6fb0864f84cceffbe4" + integrity sha512-OqlwJq1BdpB83BZXTqI+dNcA6uYk6qk4u9Cgnt64Y+XS7dwdbp/mobx8S2KXf2AXH+scNmA/UVK3SEFHR3vHZA== + +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== + +fs-extra@10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.0.tgz#9ff61b655dde53fb34a82df84bb214ce802e17c1" + integrity sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-extra@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.1.tgz#27de43b4320e833f6867cc044bfce29fdf0ef3b8" + integrity sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs-extra@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-minipass@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7" + integrity sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA== + dependencies: + minipass "^2.6.0" + +fs-minipass@^2.0.0, fs-minipass@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +fsevents@2.3.2, fsevents@^2.3.2, fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +ftp@^0.3.10: + version "0.3.10" + resolved "https://registry.yarnpkg.com/ftp/-/ftp-0.3.10.tgz#9197d861ad8142f3e63d5a83bfe4c59f7330885d" + integrity sha1-kZfYYa2BQvPmPVqDv+TFn3MwiF0= + dependencies: + readable-stream "1.1.x" + xregexp "2.0.0" + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= + +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.0.1, get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" + integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + +get-package-type@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" + integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== + +get-pkg-repo@^4.0.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/get-pkg-repo/-/get-pkg-repo-4.2.1.tgz#75973e1c8050c73f48190c52047c4cee3acbf385" + integrity sha512-2+QbHjFRfGB74v/pYWjd5OhU3TDIC2Gv/YKUTk/tCvAz0pkn/Mz6P3uByuBimLOcPvN2jYdScl3xGFSrx0jEcA== + dependencies: + "@hutson/parse-repository-url" "^3.0.0" + hosted-git-info "^4.0.0" + through2 "^2.0.0" + yargs "^16.2.0" + +get-port@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193" + integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ== + +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +get-symbol-description@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" + integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.1" + +get-uri@3: + version "3.0.2" + resolved "https://registry.yarnpkg.com/get-uri/-/get-uri-3.0.2.tgz#f0ef1356faabc70e1f9404fa3b66b2ba9bfc725c" + integrity sha512-+5s0SJbGoyiJTZZ2JTpFPLMPSch72KEqGOTvQsBqg0RBWvwhWUSYZFAtz3TPW0GXJuLBJPts1E241iHg+VRfhg== + dependencies: + "@tootallnate/once" "1" + data-uri-to-buffer "3" + debug "4" + file-uri-to-path "2" + fs-extra "^8.1.0" + ftp "^0.3.10" + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= + dependencies: + assert-plus "^1.0.0" + +git-raw-commits@^2.0.8: + version "2.0.11" + resolved "https://registry.yarnpkg.com/git-raw-commits/-/git-raw-commits-2.0.11.tgz#bc3576638071d18655e1cc60d7f524920008d723" + integrity sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A== + dependencies: + dargs "^7.0.0" + lodash "^4.17.15" + meow "^8.0.0" + split2 "^3.0.0" + through2 "^4.0.0" + +git-remote-origin-url@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/git-remote-origin-url/-/git-remote-origin-url-2.0.0.tgz#5282659dae2107145a11126112ad3216ec5fa65f" + integrity sha1-UoJlna4hBxRaERJhEq0yFuxfpl8= + dependencies: + gitconfiglocal "^1.0.0" + pify "^2.3.0" + +git-semver-tags@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/git-semver-tags/-/git-semver-tags-4.1.1.tgz#63191bcd809b0ec3e151ba4751c16c444e5b5780" + integrity sha512-OWyMt5zBe7xFs8vglMmhM9lRQzCWL3WjHtxNNfJTMngGym7pC1kh8sP6jevfydJ6LP3ZvGxfb6ABYgPUM0mtsA== + dependencies: + meow "^8.0.0" + semver "^6.0.0" + +git-up@^4.0.0: + version "4.0.5" + resolved "https://registry.yarnpkg.com/git-up/-/git-up-4.0.5.tgz#e7bb70981a37ea2fb8fe049669800a1f9a01d759" + integrity sha512-YUvVDg/vX3d0syBsk/CKUTib0srcQME0JyHkL5BaYdwLsiCslPWmDSi8PUMo9pXYjrryMcmsCoCgsTpSCJEQaA== + dependencies: + is-ssh "^1.3.0" + parse-url "^6.0.0" + +git-url-parse@^11.4.4: + version "11.6.0" + resolved "https://registry.yarnpkg.com/git-url-parse/-/git-url-parse-11.6.0.tgz#c634b8de7faa66498a2b88932df31702c67df605" + integrity sha512-WWUxvJs5HsyHL6L08wOusa/IXYtMuCAhrMmnTjQPpBU0TTHyDhnOATNH3xNQz7YOQUsqIIPTGr4xiVti1Hsk5g== + dependencies: + git-up "^4.0.0" + +gitconfiglocal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz#41d045f3851a5ea88f03f24ca1c6178114464b9b" + integrity sha1-QdBF84UaXqiPA/JMocYXgRRGS5s= + dependencies: + ini "^1.3.2" + +glob-parent@^5.1.1, glob-parent@^5.1.2, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.1: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.1.7, glob@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +globals@^13.6.0, globals@^13.9.0: + version "13.13.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.13.0.tgz#ac32261060d8070e2719dd6998406e27d2b5727b" + integrity sha512-EQ7Q18AJlPwp3vUDL4mKA0KXrXyNIQyWon6T6XQiBQF0XHvRsiCSrWmmeATpUzdJN2HhWZU6Pdl0a9zdep5p6A== + dependencies: + type-fest "^0.20.2" + +globby@^11.0.1, globby@^11.0.2, globby@^11.0.4: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + +graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.3, graceful-fs@^4.2.4, graceful-fs@^4.2.9: + version "4.2.9" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" + integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== + +handlebars@^4.7.7: + version "4.7.7" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1" + integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA== + dependencies: + minimist "^1.2.5" + neo-async "^2.6.0" + source-map "^0.6.1" + wordwrap "^1.0.0" + optionalDependencies: + uglify-js "^3.1.4" + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= + +har-validator@~5.1.3: + version "5.1.5" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" + integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== + dependencies: + ajv "^6.12.3" + har-schema "^2.0.0" + +hard-rejection@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883" + integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA== + +has-bigints@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" + integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-symbols@^1.0.1, has-symbols@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has-tostringtag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" + integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== + dependencies: + has-symbols "^1.0.2" + +has-unicode@^2.0.0, has-unicode@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +hash-sum@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/hash-sum/-/hash-sum-2.0.0.tgz#81d01bb5de8ea4a214ad5d6ead1b523460b0b45a" + integrity sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg== + +header-case@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/header-case/-/header-case-2.0.4.tgz#5a42e63b55177349cf405beb8d775acabb92c063" + integrity sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q== + dependencies: + capital-case "^1.0.4" + tslib "^2.0.3" + +hosted-git-info@^2.1.4: + version "2.8.9" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" + integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== + +hosted-git-info@^4.0.0, hosted-git-info@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.1.0.tgz#827b82867e9ff1c8d0c4d9d53880397d2c86d224" + integrity sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA== + dependencies: + lru-cache "^6.0.0" + +html-encoding-sniffer@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3" + integrity sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ== + dependencies: + whatwg-encoding "^1.0.5" + +html-escaper@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" + integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== + +http-cache-semantics@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" + integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-proxy-agent@^4.0.0, http-proxy-agent@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" + integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== + dependencies: + "@tootallnate/once" "1" + agent-base "6" + debug "4" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +https-proxy-agent@5, https-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" + integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== + dependencies: + agent-base "6" + debug "4" + +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + +humanize-ms@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" + integrity sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0= + dependencies: + ms "^2.0.0" + +husky-init@7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/husky-init/-/husky-init-7.0.0.tgz#aa2e9199245b2764a0dd4055adc8b8a295f4e4ed" + integrity sha512-SUlmIjn/WOsqviqr/Mu9xH9wxD64hEeStdu4G9Ynv+9M6mwJkT/YZCV8pp/bWW6UaJJ7Iyc/mjB845QvDmTs3g== + dependencies: + husky "^7.0.0" + +husky@^7.0.0: + version "7.0.4" + resolved "https://registry.yarnpkg.com/husky/-/husky-7.0.4.tgz#242048245dc49c8fb1bf0cc7cfb98dd722531535" + integrity sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ== + +iconv-lite@0.4.24, iconv-lite@^0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +iconv-lite@^0.6.2: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +ieee754@1.1.13: + version "1.1.13" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" + integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== + +ieee754@^1.1.13, ieee754@^1.1.4: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +ignore-walk@^3.0.3: + version "3.0.4" + resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.4.tgz#c9a09f69b7c7b479a5d74ac1a3c0d4236d2a6335" + integrity sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ== + dependencies: + minimatch "^3.0.4" + +ignore@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" + integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== + +ignore@^5.1.1, ignore@^5.1.8, ignore@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" + integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== + +import-fresh@^3.0.0, import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +import-local@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" + integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + +infer-owner@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" + integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ini@^1.3.2, ini@^1.3.4: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + +init-package-json@^2.0.2: + version "2.0.5" + resolved "https://registry.yarnpkg.com/init-package-json/-/init-package-json-2.0.5.tgz#78b85f3c36014db42d8f32117252504f68022646" + integrity sha512-u1uGAtEFu3VA6HNl/yUWw57jmKEMx8SKOxHhxjGnOFUiIlFnohKDFg4ZrPpv9wWqk44nDxGJAtqjdQFm+9XXQA== + dependencies: + npm-package-arg "^8.1.5" + promzard "^0.3.0" + read "~1.0.1" + read-package-json "^4.1.1" + semver "^7.3.5" + validate-npm-package-license "^3.0.4" + validate-npm-package-name "^3.0.0" + +inquirer@^7.3.3: + version "7.3.3" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003" + integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA== + dependencies: + ansi-escapes "^4.2.1" + chalk "^4.1.0" + cli-cursor "^3.1.0" + cli-width "^3.0.0" + external-editor "^3.0.3" + figures "^3.0.0" + lodash "^4.17.19" + mute-stream "0.0.8" + run-async "^2.4.0" + rxjs "^6.6.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + through "^2.3.6" + +internal-slot@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" + integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA== + dependencies: + get-intrinsic "^1.1.0" + has "^1.0.3" + side-channel "^1.0.4" + +io-ts-types@0.5.16: + version "0.5.16" + resolved "https://registry.yarnpkg.com/io-ts-types/-/io-ts-types-0.5.16.tgz#e9eed75371e217c97050cc507915e8eedc250946" + integrity sha512-h9noYVfY9rlbmKI902SJdnV/06jgiT2chxG6lYDxaYNp88HscPi+SBCtmcU+m0E7WT5QSwt7sIMj93+qu0FEwQ== + +io-ts@2.2.16: + version "2.2.16" + resolved "https://registry.yarnpkg.com/io-ts/-/io-ts-2.2.16.tgz#597dffa03db1913fc318c9c6df6931cb4ed808b2" + integrity sha512-y5TTSa6VP6le0hhmIyN0dqEXkrZeJLeC5KApJq6VLci3UEKF80lZ+KuoUs02RhBxNWlrqSNxzfI7otLX1Euv8Q== + +ip-num@1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/ip-num/-/ip-num-1.3.3.tgz#84a4e76c9d37c3238b9bef4fa915d2192bff67af" + integrity sha512-1QsiMKglDaemuIktincG1ntr3DvVTV/pU++eyG7vIm4xd+gvtJ9eoB34RRbI9YTqn1U5og16n7+1RgwLhv4RmA== + dependencies: + big-integer "^1.6.48" + +ip@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" + integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= + +is-arguments@^1.0.4, is-arguments@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" + integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= + +is-arrayish@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + +is-bigint@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + dependencies: + has-bigints "^1.0.1" + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-boolean-object@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-buffer@~1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + +is-callable@^1.1.4, is-callable@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945" + integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== + +is-ci@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" + integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== + dependencies: + ci-info "^2.0.0" + +is-core-module@^2.5.0, is-core-module@^2.8.0, is-core-module@^2.8.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211" + integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA== + dependencies: + has "^1.0.3" + +is-date-object@^1.0.1, is-date-object@^1.0.2: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== + dependencies: + has-tostringtag "^1.0.0" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-fullwidth-code-point@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz#fae3167c729e7463f8461ce512b080a49268aa88" + integrity sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ== + +is-generator-fn@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" + integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-lambda@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" + integrity sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU= + +is-map@^2.0.1, is-map@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127" + integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg== + +is-negative-zero@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" + integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== + +is-number-object@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.6.tgz#6a7aaf838c7f0686a50b4553f7e54a96494e89f0" + integrity sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g== + dependencies: + has-tostringtag "^1.0.0" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-obj@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" + integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== + +is-path-cwd@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" + integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== + +is-path-inside@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" + integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= + +is-plain-obj@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + +is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-plain-object@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" + integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== + +is-potential-custom-element-name@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" + integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== + +is-regex@^1.1.1, is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-set@^2.0.1, is-set@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.2.tgz#90755fa4c2562dc1c5d4024760d6119b94ca18ec" + integrity sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g== + +is-shared-array-buffer@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz#97b0c85fbdacb59c9c446fe653b82cf2b5b7cfe6" + integrity sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA== + +is-ssh@^1.3.0: + version "1.3.3" + resolved "https://registry.yarnpkg.com/is-ssh/-/is-ssh-1.3.3.tgz#7f133285ccd7f2c2c7fc897b771b53d95a2b2c7e" + integrity sha512-NKzJmQzJfEEma3w5cJNcUMxoXfDjz0Zj0eyCalHn2E6VOwlzjZo0yuO2fcBSf8zhFuVCL/82/r5gRcoi6aEPVQ== + dependencies: + protocols "^1.1.0" + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac" + integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== + +is-string@^1.0.5, is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" + +is-symbol@^1.0.2, is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== + dependencies: + has-symbols "^1.0.2" + +is-text-path@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-text-path/-/is-text-path-1.0.1.tgz#4e1aa0fb51bfbcb3e92688001397202c1775b66e" + integrity sha1-Thqg+1G/vLPpJogAE5cgLBd1tm4= + dependencies: + text-extensions "^1.0.0" + +is-typed-array@^1.1.7: + version "1.1.8" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.8.tgz#cbaa6585dc7db43318bc5b89523ea384a6f65e79" + integrity sha512-HqH41TNZq2fgtGT8WHVFVJhBVGuY3AnP3Q36K8JKXUxSxRgk/d+7NjmwG2vo2mYmXK8UYZKu0qH8bVP5gEisjA== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + es-abstract "^1.18.5" + foreach "^2.0.5" + has-tostringtag "^1.0.0" + +is-typedarray@^1.0.0, is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + +is-weakmap@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2" + integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA== + +is-weakref@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" + +is-weakset@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.2.tgz#4569d67a747a1ce5a994dfd4ef6dcea76e7c0a1d" + integrity sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.1" + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= + +isarray@^1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= + +istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" + integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== + +istanbul-lib-instrument@^5.0.4, istanbul-lib-instrument@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.1.0.tgz#7b49198b657b27a730b8e9cb601f1e1bff24c59a" + integrity sha512-czwUz525rkOFDJxfKK6mYfIs9zBKILyrZQxjz3ABhjQXhbhFsSbo1HW/BFcsDnfJYJWA6thRR5/TUY2qs5W99Q== + dependencies: + "@babel/core" "^7.12.3" + "@babel/parser" "^7.14.7" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.2.0" + semver "^6.3.0" + +istanbul-lib-report@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6" + integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw== + dependencies: + istanbul-lib-coverage "^3.0.0" + make-dir "^3.0.0" + supports-color "^7.1.0" + +istanbul-lib-source-maps@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#895f3a709fcfba34c6de5a42939022f3e4358551" + integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== + dependencies: + debug "^4.1.1" + istanbul-lib-coverage "^3.0.0" + source-map "^0.6.1" + +istanbul-reports@^3.1.3: + version "3.1.4" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.4.tgz#1b6f068ecbc6c331040aab5741991273e609e40c" + integrity sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw== + dependencies: + html-escaper "^2.0.0" + istanbul-lib-report "^3.0.0" + +jest-changed-files@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-27.5.1.tgz#a348aed00ec9bf671cc58a66fcbe7c3dfd6a68f5" + integrity sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw== + dependencies: + "@jest/types" "^27.5.1" + execa "^5.0.0" + throat "^6.0.1" + +jest-circus@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-27.5.1.tgz#37a5a4459b7bf4406e53d637b49d22c65d125ecc" + integrity sha512-D95R7x5UtlMA5iBYsOHFFbMD/GVA4R/Kdq15f7xYWUfWHBto9NYRsOvnSauTgdF+ogCpJ4tyKOXhUifxS65gdw== + dependencies: + "@jest/environment" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/node" "*" + chalk "^4.0.0" + co "^4.6.0" + dedent "^0.7.0" + expect "^27.5.1" + is-generator-fn "^2.0.0" + jest-each "^27.5.1" + jest-matcher-utils "^27.5.1" + jest-message-util "^27.5.1" + jest-runtime "^27.5.1" + jest-snapshot "^27.5.1" + jest-util "^27.5.1" + pretty-format "^27.5.1" + slash "^3.0.0" + stack-utils "^2.0.3" + throat "^6.0.1" + +jest-cli@^27.4.3: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-27.5.1.tgz#278794a6e6458ea8029547e6c6cbf673bd30b145" + integrity sha512-Hc6HOOwYq4/74/c62dEE3r5elx8wjYqxY0r0G/nFrLDPMFRu6RA/u8qINOIkvhxG7mMQ5EJsOGfRpI8L6eFUVw== + dependencies: + "@jest/core" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/types" "^27.5.1" + chalk "^4.0.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + import-local "^3.0.2" + jest-config "^27.5.1" + jest-util "^27.5.1" + jest-validate "^27.5.1" + prompts "^2.0.1" + yargs "^16.2.0" + +jest-config@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-27.5.1.tgz#5c387de33dca3f99ad6357ddeccd91bf3a0e4a41" + integrity sha512-5sAsjm6tGdsVbW9ahcChPAFCk4IlkQUknH5AvKjuLTSlcO/wCZKyFdn7Rg0EkC+OGgWODEy2hDpWB1PgzH0JNA== + dependencies: + "@babel/core" "^7.8.0" + "@jest/test-sequencer" "^27.5.1" + "@jest/types" "^27.5.1" + babel-jest "^27.5.1" + chalk "^4.0.0" + ci-info "^3.2.0" + deepmerge "^4.2.2" + glob "^7.1.1" + graceful-fs "^4.2.9" + jest-circus "^27.5.1" + jest-environment-jsdom "^27.5.1" + jest-environment-node "^27.5.1" + jest-get-type "^27.5.1" + jest-jasmine2 "^27.5.1" + jest-regex-util "^27.5.1" + jest-resolve "^27.5.1" + jest-runner "^27.5.1" + jest-util "^27.5.1" + jest-validate "^27.5.1" + micromatch "^4.0.4" + parse-json "^5.2.0" + pretty-format "^27.5.1" + slash "^3.0.0" + strip-json-comments "^3.1.1" + +jest-diff@^27.0.0, jest-diff@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.5.1.tgz#a07f5011ac9e6643cf8a95a462b7b1ecf6680def" + integrity sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw== + dependencies: + chalk "^4.0.0" + diff-sequences "^27.5.1" + jest-get-type "^27.5.1" + pretty-format "^27.5.1" + +jest-docblock@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-27.5.1.tgz#14092f364a42c6108d42c33c8cf30e058e25f6c0" + integrity sha512-rl7hlABeTsRYxKiUfpHrQrG4e2obOiTQWfMEH3PxPjOtdsfLQO4ReWSZaQ7DETm4xu07rl4q/h4zcKXyU0/OzQ== + dependencies: + detect-newline "^3.0.0" + +jest-each@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-27.5.1.tgz#5bc87016f45ed9507fed6e4702a5b468a5b2c44e" + integrity sha512-1Ff6p+FbhT/bXQnEouYy00bkNSY7OUpfIcmdl8vZ31A1UUaurOLPA8a8BbJOF2RDUElwJhmeaV7LnagI+5UwNQ== + dependencies: + "@jest/types" "^27.5.1" + chalk "^4.0.0" + jest-get-type "^27.5.1" + jest-util "^27.5.1" + pretty-format "^27.5.1" + +jest-environment-jsdom@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-27.5.1.tgz#ea9ccd1fc610209655a77898f86b2b559516a546" + integrity sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw== + dependencies: + "@jest/environment" "^27.5.1" + "@jest/fake-timers" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/node" "*" + jest-mock "^27.5.1" + jest-util "^27.5.1" + jsdom "^16.6.0" + +jest-environment-node@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-27.5.1.tgz#dedc2cfe52fab6b8f5714b4808aefa85357a365e" + integrity sha512-Jt4ZUnxdOsTGwSRAfKEnE6BcwsSPNOijjwifq5sDFSA2kesnXTvNqKHYgM0hDq3549Uf/KzdXNYn4wMZJPlFLw== + dependencies: + "@jest/environment" "^27.5.1" + "@jest/fake-timers" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/node" "*" + jest-mock "^27.5.1" + jest-util "^27.5.1" + +jest-get-type@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1" + integrity sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw== + +jest-haste-map@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.5.1.tgz#9fd8bd7e7b4fa502d9c6164c5640512b4e811e7f" + integrity sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng== + dependencies: + "@jest/types" "^27.5.1" + "@types/graceful-fs" "^4.1.2" + "@types/node" "*" + anymatch "^3.0.3" + fb-watchman "^2.0.0" + graceful-fs "^4.2.9" + jest-regex-util "^27.5.1" + jest-serializer "^27.5.1" + jest-util "^27.5.1" + jest-worker "^27.5.1" + micromatch "^4.0.4" + walker "^1.0.7" + optionalDependencies: + fsevents "^2.3.2" + +jest-jasmine2@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-27.5.1.tgz#a037b0034ef49a9f3d71c4375a796f3b230d1ac4" + integrity sha512-jtq7VVyG8SqAorDpApwiJJImd0V2wv1xzdheGHRGyuT7gZm6gG47QEskOlzsN1PG/6WNaCo5pmwMHDf3AkG2pQ== + dependencies: + "@jest/environment" "^27.5.1" + "@jest/source-map" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/node" "*" + chalk "^4.0.0" + co "^4.6.0" + expect "^27.5.1" + is-generator-fn "^2.0.0" + jest-each "^27.5.1" + jest-matcher-utils "^27.5.1" + jest-message-util "^27.5.1" + jest-runtime "^27.5.1" + jest-snapshot "^27.5.1" + jest-util "^27.5.1" + pretty-format "^27.5.1" + throat "^6.0.1" + +jest-junit@13.0.0: + version "13.0.0" + resolved "https://registry.yarnpkg.com/jest-junit/-/jest-junit-13.0.0.tgz#479be347457aad98ae8a5983a23d7c3ec526c9a3" + integrity sha512-JSHR+Dhb32FGJaiKkqsB7AR3OqWKtldLd6ZH2+FJ8D4tsweb8Id8zEVReU4+OlrRO1ZluqJLQEETm+Q6/KilBg== + dependencies: + mkdirp "^1.0.4" + strip-ansi "^6.0.1" + uuid "^8.3.2" + xml "^1.0.1" + +jest-leak-detector@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-27.5.1.tgz#6ec9d54c3579dd6e3e66d70e3498adf80fde3fb8" + integrity sha512-POXfWAMvfU6WMUXftV4HolnJfnPOGEu10fscNCA76KBpRRhcMN2c8d3iT2pxQS3HLbA+5X4sOUPzYO2NUyIlHQ== + dependencies: + jest-get-type "^27.5.1" + pretty-format "^27.5.1" + +jest-matcher-utils@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz#9c0cdbda8245bc22d2331729d1091308b40cf8ab" + integrity sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw== + dependencies: + chalk "^4.0.0" + jest-diff "^27.5.1" + jest-get-type "^27.5.1" + pretty-format "^27.5.1" + +jest-message-util@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-27.5.1.tgz#bdda72806da10d9ed6425e12afff38cd1458b6cf" + integrity sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g== + dependencies: + "@babel/code-frame" "^7.12.13" + "@jest/types" "^27.5.1" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^27.5.1" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-mock@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-27.5.1.tgz#19948336d49ef4d9c52021d34ac7b5f36ff967d6" + integrity sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og== + dependencies: + "@jest/types" "^27.5.1" + "@types/node" "*" + +jest-pnp-resolver@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c" + integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w== + +jest-regex-util@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-27.5.1.tgz#4da143f7e9fd1e542d4aa69617b38e4a78365b95" + integrity sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg== + +jest-resolve-dependencies@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-27.5.1.tgz#d811ecc8305e731cc86dd79741ee98fed06f1da8" + integrity sha512-QQOOdY4PE39iawDn5rzbIePNigfe5B9Z91GDD1ae/xNDlu9kaat8QQ5EKnNmVWPV54hUdxCVwwj6YMgR2O7IOg== + dependencies: + "@jest/types" "^27.5.1" + jest-regex-util "^27.5.1" + jest-snapshot "^27.5.1" + +jest-resolve@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-27.5.1.tgz#a2f1c5a0796ec18fe9eb1536ac3814c23617b384" + integrity sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw== + dependencies: + "@jest/types" "^27.5.1" + chalk "^4.0.0" + graceful-fs "^4.2.9" + jest-haste-map "^27.5.1" + jest-pnp-resolver "^1.2.2" + jest-util "^27.5.1" + jest-validate "^27.5.1" + resolve "^1.20.0" + resolve.exports "^1.1.0" + slash "^3.0.0" + +jest-runner@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-27.5.1.tgz#071b27c1fa30d90540805c5645a0ec167c7b62e5" + integrity sha512-g4NPsM4mFCOwFKXO4p/H/kWGdJp9V8kURY2lX8Me2drgXqG7rrZAx5kv+5H7wtt/cdFIjhqYx1HrlqWHaOvDaQ== + dependencies: + "@jest/console" "^27.5.1" + "@jest/environment" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/node" "*" + chalk "^4.0.0" + emittery "^0.8.1" + graceful-fs "^4.2.9" + jest-docblock "^27.5.1" + jest-environment-jsdom "^27.5.1" + jest-environment-node "^27.5.1" + jest-haste-map "^27.5.1" + jest-leak-detector "^27.5.1" + jest-message-util "^27.5.1" + jest-resolve "^27.5.1" + jest-runtime "^27.5.1" + jest-util "^27.5.1" + jest-worker "^27.5.1" + source-map-support "^0.5.6" + throat "^6.0.1" + +jest-runtime@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-27.5.1.tgz#4896003d7a334f7e8e4a53ba93fb9bcd3db0a1af" + integrity sha512-o7gxw3Gf+H2IGt8fv0RiyE1+r83FJBRruoA+FXrlHw6xEyBsU8ugA6IPfTdVyA0w8HClpbK+DGJxH59UrNMx8A== + dependencies: + "@jest/environment" "^27.5.1" + "@jest/fake-timers" "^27.5.1" + "@jest/globals" "^27.5.1" + "@jest/source-map" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" + chalk "^4.0.0" + cjs-module-lexer "^1.0.0" + collect-v8-coverage "^1.0.0" + execa "^5.0.0" + glob "^7.1.3" + graceful-fs "^4.2.9" + jest-haste-map "^27.5.1" + jest-message-util "^27.5.1" + jest-mock "^27.5.1" + jest-regex-util "^27.5.1" + jest-resolve "^27.5.1" + jest-snapshot "^27.5.1" + jest-util "^27.5.1" + slash "^3.0.0" + strip-bom "^4.0.0" + +jest-serializer@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-27.5.1.tgz#81438410a30ea66fd57ff730835123dea1fb1f64" + integrity sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w== + dependencies: + "@types/node" "*" + graceful-fs "^4.2.9" + +jest-snapshot@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-27.5.1.tgz#b668d50d23d38054a51b42c4039cab59ae6eb6a1" + integrity sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA== + dependencies: + "@babel/core" "^7.7.2" + "@babel/generator" "^7.7.2" + "@babel/plugin-syntax-typescript" "^7.7.2" + "@babel/traverse" "^7.7.2" + "@babel/types" "^7.0.0" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/babel__traverse" "^7.0.4" + "@types/prettier" "^2.1.5" + babel-preset-current-node-syntax "^1.0.0" + chalk "^4.0.0" + expect "^27.5.1" + graceful-fs "^4.2.9" + jest-diff "^27.5.1" + jest-get-type "^27.5.1" + jest-haste-map "^27.5.1" + jest-matcher-utils "^27.5.1" + jest-message-util "^27.5.1" + jest-util "^27.5.1" + natural-compare "^1.4.0" + pretty-format "^27.5.1" + semver "^7.3.2" + +jest-util@^27.0.0, jest-util@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.5.1.tgz#3ba9771e8e31a0b85da48fe0b0891fb86c01c2f9" + integrity sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw== + dependencies: + "@jest/types" "^27.5.1" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + +jest-validate@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-27.5.1.tgz#9197d54dc0bdb52260b8db40b46ae668e04df067" + integrity sha512-thkNli0LYTmOI1tDB3FI1S1RTp/Bqyd9pTarJwL87OIBFuqEb5Apv5EaApEudYg4g86e3CT6kM0RowkhtEnCBQ== + dependencies: + "@jest/types" "^27.5.1" + camelcase "^6.2.0" + chalk "^4.0.0" + jest-get-type "^27.5.1" + leven "^3.1.0" + pretty-format "^27.5.1" + +jest-watcher@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-27.5.1.tgz#71bd85fb9bde3a2c2ec4dc353437971c43c642a2" + integrity sha512-z676SuD6Z8o8qbmEGhoEUFOM1+jfEiL3DXHK/xgEiG2EyNYfFG60jluWcupY6dATjfEsKQuibReS1djInQnoVw== + dependencies: + "@jest/test-result" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + jest-util "^27.5.1" + string-length "^4.0.1" + +jest-worker@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +jest@27.4.3: + version "27.4.3" + resolved "https://registry.yarnpkg.com/jest/-/jest-27.4.3.tgz#cf7d1876a84c70efece2e01e4f9dfc2e464d9cbb" + integrity sha512-jwsfVABBzuN3Atm+6h6vIEpTs9+VApODLt4dk2qv1WMOpb1weI1IIZfuwpMiWZ62qvWj78MvdvMHIYdUfqrFaA== + dependencies: + "@jest/core" "^27.4.3" + import-local "^3.0.2" + jest-cli "^27.4.3" + +jmespath@0.15.0: + version "0.15.0" + resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217" + integrity sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc= + +jmespath@0.16.0: + version "0.16.0" + resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.16.0.tgz#b15b0a85dfd4d930d43e69ed605943c802785076" + integrity sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw== + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@4.1.0, js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +js-yaml@^3.13.1, js-yaml@^3.14.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= + +jsdom@^16.6.0: + version "16.7.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.7.0.tgz#918ae71965424b197c819f8183a754e18977b710" + integrity sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw== + dependencies: + abab "^2.0.5" + acorn "^8.2.4" + acorn-globals "^6.0.0" + cssom "^0.4.4" + cssstyle "^2.3.0" + data-urls "^2.0.0" + decimal.js "^10.2.1" + domexception "^2.0.1" + escodegen "^2.0.0" + form-data "^3.0.0" + html-encoding-sniffer "^2.0.1" + http-proxy-agent "^4.0.1" + https-proxy-agent "^5.0.0" + is-potential-custom-element-name "^1.0.1" + nwsapi "^2.2.0" + parse5 "6.0.1" + saxes "^5.0.1" + symbol-tree "^3.2.4" + tough-cookie "^4.0.0" + w3c-hr-time "^1.0.2" + w3c-xmlserializer "^2.0.0" + webidl-conversions "^6.1.0" + whatwg-encoding "^1.0.5" + whatwg-mimetype "^2.3.0" + whatwg-url "^8.5.0" + ws "^7.4.6" + xml-name-validator "^3.0.0" + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +jsii-pacmak@1.47.0: + version "1.47.0" + resolved "https://registry.yarnpkg.com/jsii-pacmak/-/jsii-pacmak-1.47.0.tgz#82e687b5444847d79f1030b9ef480b68b63c0e91" + integrity sha512-VGrHZsK2jv7NuPBULvJBJGF0hLpGwdJa7esI2qzOeX1UbHxXSOah9oboheYELbX5KCH3hSe2uDbcPSnEqIT68Q== + dependencies: + "@jsii/check-node" "1.47.0" + "@jsii/spec" "^1.47.0" + clone "^2.1.2" + codemaker "^1.47.0" + commonmark "^0.30.0" + escape-string-regexp "^4.0.0" + fs-extra "^9.1.0" + jsii-reflect "^1.47.0" + jsii-rosetta "^1.47.0" + semver "^7.3.5" + spdx-license-list "^6.4.0" + xmlbuilder "^15.1.1" + yargs "^16.2.0" + +jsii-reflect@^1.47.0: + version "1.55.1" + resolved "https://registry.yarnpkg.com/jsii-reflect/-/jsii-reflect-1.55.1.tgz#b192d4f1121f1a142581614fd68e26f25abe2dc9" + integrity sha512-/Ak+sCuIjJaRCflCWT2UKPdT88EQhbPYLhtF7F42uuUr2tchlNkybNE15bigZbtqLw7SP1fp/6Dedujvf90N9Q== + dependencies: + "@jsii/check-node" "1.55.1" + "@jsii/spec" "^1.55.1" + chalk "^4" + fs-extra "^9.1.0" + oo-ascii-tree "^1.55.1" + yargs "^16.2.0" + +jsii-rosetta@^1.47.0: + version "1.55.1" + resolved "https://registry.yarnpkg.com/jsii-rosetta/-/jsii-rosetta-1.55.1.tgz#30b44111d146534c42c8fef77931d3fdb888de7d" + integrity sha512-ZUzuO2JgnxE01tgIdZorsUZj5jiHP8uxeLDU/vsnmnAU2ZbMHFDT1cWacoAKESDnCyFF8VRCuPXHx8e5/SOXig== + dependencies: + "@jsii/check-node" "1.55.1" + "@jsii/spec" "1.55.1" + "@xmldom/xmldom" "^0.8.1" + commonmark "^0.30.0" + fast-glob "^3.2.11" + fs-extra "^9.1.0" + jsii "1.55.1" + semver "^7.3.5" + semver-intersect "^1.4.0" + sort-json "^2.0.1" + typescript "~3.9.10" + workerpool "^6.2.0" + yargs "^16.2.0" + +jsii@1.47.0: + version "1.47.0" + resolved "https://registry.yarnpkg.com/jsii/-/jsii-1.47.0.tgz#79b85c7e90098f93863e9a38eca1a4c42a382aaa" + integrity sha512-cJ1cQfanSl+0vLVRJTwaWdx0dZ4DxIxYhydRlhuv/EFbQzeAVosy6fEnZ5IlPtWR89tF3k89R6w8DHVhPigjjQ== + dependencies: + "@jsii/check-node" "1.47.0" + "@jsii/spec" "^1.47.0" + case "^1.6.3" + colors "^1.4.0" + deep-equal "^2.0.5" + fs-extra "^9.1.0" + log4js "^6.3.0" + semver "^7.3.5" + semver-intersect "^1.4.0" + sort-json "^2.0.0" + spdx-license-list "^6.4.0" + typescript "~3.9.10" + yargs "^16.2.0" + +jsii@1.55.1: + version "1.55.1" + resolved "https://registry.yarnpkg.com/jsii/-/jsii-1.55.1.tgz#e4018b4d5017b57857c608d2acb850f07a5f94a3" + integrity sha512-9L6BztDV8PwNY5C+vwuLRJTzijh5Kyh3eijaz8NS11Jc7rTeTN8AvLxyWsIaPO+ITTP4JTsDKOU3tBaoWabRzA== + dependencies: + "@jsii/check-node" "1.55.1" + "@jsii/spec" "^1.55.1" + case "^1.6.3" + chalk "^4" + deep-equal "^2.0.5" + fs-extra "^9.1.0" + log4js "^6.4.2" + semver "^7.3.5" + semver-intersect "^1.4.0" + sort-json "^2.0.1" + spdx-license-list "^6.4.0" + typescript "~3.9.10" + yargs "^16.2.0" + +json-parse-better-errors@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" + integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== + +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +json-schema@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" + integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= + +json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + +json5@2.x, json5@^2.1.2: + version "2.2.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" + integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== + +json5@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" + integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + dependencies: + minimist "^1.2.0" + +jsonc-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.0.0.tgz#abdd785701c7e7eaca8a9ec8cf070ca51a745a22" + integrity sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA== + +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= + optionalDependencies: + graceful-fs "^4.1.6" + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +jsonparse@^1.2.0, jsonparse@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" + integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= + +jsonschema@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/jsonschema/-/jsonschema-1.4.0.tgz#1afa34c4bc22190d8e42271ec17ac8b3404f87b2" + integrity sha512-/YgW6pRMr6M7C+4o8kS+B/2myEpHCrxO4PEWnqJNBFMjn7EWXqlQ4tGwL6xTHeRplwuZmcAncdvfOad1nT2yMw== + +jsprim@^1.2.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb" + integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw== + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.4.0" + verror "1.10.0" + +kind-of@^6.0.2, kind-of@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +kleur@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + +kuler@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" + integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== + +lazystream@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.1.tgz#494c831062f1f9408251ec44db1cba29242a2638" + integrity sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw== + dependencies: + readable-stream "^2.0.5" + +lerna@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/lerna/-/lerna-4.0.0.tgz#b139d685d50ea0ca1be87713a7c2f44a5b678e9e" + integrity sha512-DD/i1znurfOmNJb0OBw66NmNqiM8kF6uIrzrJ0wGE3VNdzeOhz9ziWLYiRaZDGGwgbcjOo6eIfcx9O5Qynz+kg== + dependencies: + "@lerna/add" "4.0.0" + "@lerna/bootstrap" "4.0.0" + "@lerna/changed" "4.0.0" + "@lerna/clean" "4.0.0" + "@lerna/cli" "4.0.0" + "@lerna/create" "4.0.0" + "@lerna/diff" "4.0.0" + "@lerna/exec" "4.0.0" + "@lerna/import" "4.0.0" + "@lerna/info" "4.0.0" + "@lerna/init" "4.0.0" + "@lerna/link" "4.0.0" + "@lerna/list" "4.0.0" + "@lerna/publish" "4.0.0" + "@lerna/run" "4.0.0" + "@lerna/version" "4.0.0" + import-local "^3.0.2" + npmlog "^4.1.2" + +leven@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" + integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +libnpmaccess@^4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/libnpmaccess/-/libnpmaccess-4.0.3.tgz#dfb0e5b0a53c315a2610d300e46b4ddeb66e7eec" + integrity sha512-sPeTSNImksm8O2b6/pf3ikv4N567ERYEpeKRPSmqlNt1dTZbvgpJIzg5vAhXHpw2ISBsELFRelk0jEahj1c6nQ== + dependencies: + aproba "^2.0.0" + minipass "^3.1.1" + npm-package-arg "^8.1.2" + npm-registry-fetch "^11.0.0" + +libnpmpublish@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/libnpmpublish/-/libnpmpublish-4.0.2.tgz#be77e8bf5956131bcb45e3caa6b96a842dec0794" + integrity sha512-+AD7A2zbVeGRCFI2aO//oUmapCwy7GHqPXFJh3qpToSRNU+tXKJ2YFUgjt04LPPAf2dlEH95s6EhIHM1J7bmOw== + dependencies: + normalize-package-data "^3.0.2" + npm-package-arg "^8.1.2" + npm-registry-fetch "^11.0.0" + semver "^7.1.3" + ssri "^8.0.1" + +lilconfig@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.4.tgz#f4507d043d7058b380b6a8f5cb7bcd4b34cee082" + integrity sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA== + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +lint-staged@12.1.2: + version "12.1.2" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-12.1.2.tgz#90c571927e1371fc133e720671dd7989eab53f74" + integrity sha512-bSMcQVqMW98HLLLR2c2tZ+vnDCnx4fd+0QJBQgN/4XkdspGRPc8DGp7UuOEBe1ApCfJ+wXXumYnJmU+wDo7j9A== + dependencies: + cli-truncate "^3.1.0" + colorette "^2.0.16" + commander "^8.3.0" + debug "^4.3.2" + enquirer "^2.3.6" + execa "^5.1.1" + lilconfig "2.0.4" + listr2 "^3.13.3" + micromatch "^4.0.4" + normalize-path "^3.0.0" + object-inspect "^1.11.0" + string-argv "^0.3.1" + supports-color "^9.0.2" + yaml "^1.10.2" + +listr2@^3.13.3: + version "3.14.0" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.14.0.tgz#23101cc62e1375fd5836b248276d1d2b51fdbe9e" + integrity sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g== + dependencies: + cli-truncate "^2.1.0" + colorette "^2.0.16" + log-update "^4.0.0" + p-map "^4.0.0" + rfdc "^1.3.0" + rxjs "^7.5.1" + through "^2.3.8" + wrap-ansi "^7.0.0" + +load-json-file@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" + integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs= + dependencies: + graceful-fs "^4.1.2" + parse-json "^4.0.0" + pify "^3.0.0" + strip-bom "^3.0.0" + +load-json-file@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-6.2.0.tgz#5c7770b42cafa97074ca2848707c61662f4251a1" + integrity sha512-gUD/epcRms75Cw8RT1pUdHugZYM5ce64ucs2GEISABwkRsOQr0q2wm/MV2TKThycIe5e0ytRweW2RZxclogCdQ== + dependencies: + graceful-fs "^4.1.15" + parse-json "^5.0.0" + strip-bom "^4.0.0" + type-fest "^0.6.0" + +locate-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= + dependencies: + p-locate "^2.0.0" + path-exists "^3.0.0" + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +lodash._reinterpolate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" + integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= + +lodash.defaults@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" + integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw= + +lodash.difference@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.difference/-/lodash.difference-4.5.0.tgz#9ccb4e505d486b91651345772885a2df27fd017c" + integrity sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw= + +lodash.flatten@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" + integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8= + +lodash.ismatch@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37" + integrity sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc= + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= + +lodash.memoize@4.x: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lodash.template@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab" + integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A== + dependencies: + lodash._reinterpolate "^3.0.0" + lodash.templatesettings "^4.0.0" + +lodash.templatesettings@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz#e481310f049d3cf6d47e912ad09313b154f0fb33" + integrity sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ== + dependencies: + lodash._reinterpolate "^3.0.0" + +lodash.truncate@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" + integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM= + +lodash.union@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88" + integrity sha1-SLtQiECfFvGCFmZkHETdGqrjzYg= + +lodash@^4.17.15, lodash@^4.17.19, lodash@^4.7.0: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +log-update@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1" + integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg== + dependencies: + ansi-escapes "^4.3.0" + cli-cursor "^3.1.0" + slice-ansi "^4.0.0" + wrap-ansi "^6.2.0" + +log4js@^6.3.0, log4js@^6.4.2: + version "6.4.4" + resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.4.4.tgz#c9bc75569f3f40bba22fe1bd0677afa7a6a13bac" + integrity sha512-ncaWPsuw9Vl1CKA406hVnJLGQKy1OHx6buk8J4rE2lVW+NW5Y82G5/DIloO7NkqLOUtNPEANaWC1kZYVjXssPw== + dependencies: + date-format "^4.0.6" + debug "^4.3.4" + flatted "^3.2.5" + rfdc "^1.3.0" + streamroller "^3.0.6" + +logform@^2.2.0, logform@^2.3.2: + version "2.4.0" + resolved "https://registry.yarnpkg.com/logform/-/logform-2.4.0.tgz#131651715a17d50f09c2a2c1a524ff1a4164bcfe" + integrity sha512-CPSJw4ftjf517EhXZGGvTHHkYobo7ZCc0kvwUoOYcjfR2UVrI66RHj8MCrfAdEitdmFqbu2BYdYs8FHHZSb6iw== + dependencies: + "@colors/colors" "1.5.0" + fecha "^4.2.0" + ms "^2.1.1" + safe-stable-stringify "^2.3.1" + triple-beam "^1.3.0" + +lower-case@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" + integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== + dependencies: + tslib "^2.0.3" + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +lunr@^2.3.9: + version "2.3.9" + resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1" + integrity sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow== + +make-dir@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" + integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== + dependencies: + pify "^4.0.1" + semver "^5.6.0" + +make-dir@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" + +make-error@1.x, make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +make-fetch-happen@^8.0.9: + version "8.0.14" + resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-8.0.14.tgz#aaba73ae0ab5586ad8eaa68bd83332669393e222" + integrity sha512-EsS89h6l4vbfJEtBZnENTOFk8mCRpY5ru36Xe5bcX1KYIli2mkSHqoFsp5O1wMDvTJJzxe/4THpCTtygjeeGWQ== + dependencies: + agentkeepalive "^4.1.3" + cacache "^15.0.5" + http-cache-semantics "^4.1.0" + http-proxy-agent "^4.0.1" + https-proxy-agent "^5.0.0" + is-lambda "^1.0.1" + lru-cache "^6.0.0" + minipass "^3.1.3" + minipass-collect "^1.0.2" + minipass-fetch "^1.3.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + promise-retry "^2.0.1" + socks-proxy-agent "^5.0.0" + ssri "^8.0.0" + +make-fetch-happen@^9.0.1: + version "9.1.0" + resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz#53085a09e7971433e6765f7971bf63f4e05cb968" + integrity sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg== + dependencies: + agentkeepalive "^4.1.3" + cacache "^15.2.0" + http-cache-semantics "^4.1.0" + http-proxy-agent "^4.0.1" + https-proxy-agent "^5.0.0" + is-lambda "^1.0.1" + lru-cache "^6.0.0" + minipass "^3.1.3" + minipass-collect "^1.0.2" + minipass-fetch "^1.3.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + negotiator "^0.6.2" + promise-retry "^2.0.1" + socks-proxy-agent "^6.0.0" + ssri "^8.0.0" + +makeerror@1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" + integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== + dependencies: + tmpl "1.0.5" + +map-obj@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" + integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= + +map-obj@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.3.0.tgz#9304f906e93faae70880da102a9f1df0ea8bb05a" + integrity sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ== + +marked@^4.0.10: + version "4.0.12" + resolved "https://registry.yarnpkg.com/marked/-/marked-4.0.12.tgz#2262a4e6fd1afd2f13557726238b69a48b982f7d" + integrity sha512-hgibXWrEDNBWgGiK18j/4lkS6ihTe9sxtV4Q1OQppb/0zzyPSzoFANBa5MfsG/zgsWklmNnhm0XACZOH/0HBiQ== + +md5@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f" + integrity sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g== + dependencies: + charenc "0.0.2" + crypt "0.0.2" + is-buffer "~1.1.6" + +mdurl@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" + integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= + +meow@^8.0.0: + version "8.1.2" + resolved "https://registry.yarnpkg.com/meow/-/meow-8.1.2.tgz#bcbe45bda0ee1729d350c03cffc8395a36c4e897" + integrity sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q== + dependencies: + "@types/minimist" "^1.2.0" + camelcase-keys "^6.2.2" + decamelize-keys "^1.1.0" + hard-rejection "^2.1.0" + minimist-options "4.1.0" + normalize-package-data "^3.0.0" + read-pkg-up "^7.0.1" + redent "^3.0.0" + trim-newlines "^3.0.0" + type-fest "^0.18.0" + yargs-parser "^20.2.3" + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" + integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== + dependencies: + braces "^3.0.1" + picomatch "^2.2.3" + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12, mime-types@~2.1.19: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +min-indent@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" + integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== + +minimatch@^3.0.4, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimist-options@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" + integrity sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A== + dependencies: + arrify "^1.0.1" + is-plain-obj "^1.1.0" + kind-of "^6.0.3" + +minimist@>=1.2.2, minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: + version "1.2.6" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== + +minipass-collect@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" + integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA== + dependencies: + minipass "^3.0.0" + +minipass-fetch@^1.3.0, minipass-fetch@^1.3.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-1.4.1.tgz#d75e0091daac1b0ffd7e9d41629faff7d0c1f1b6" + integrity sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw== + dependencies: + minipass "^3.1.0" + minipass-sized "^1.0.3" + minizlib "^2.0.0" + optionalDependencies: + encoding "^0.1.12" + +minipass-flush@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373" + integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw== + dependencies: + minipass "^3.0.0" + +minipass-json-stream@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz#7edbb92588fbfc2ff1db2fc10397acb7b6b44aa7" + integrity sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg== + dependencies: + jsonparse "^1.3.1" + minipass "^3.0.0" + +minipass-pipeline@^1.2.2, minipass-pipeline@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c" + integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== + dependencies: + minipass "^3.0.0" + +minipass-sized@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/minipass-sized/-/minipass-sized-1.0.3.tgz#70ee5a7c5052070afacfbc22977ea79def353b70" + integrity sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g== + dependencies: + minipass "^3.0.0" + +minipass@^2.6.0, minipass@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6" + integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg== + dependencies: + safe-buffer "^5.1.2" + yallist "^3.0.0" + +minipass@^3.0.0, minipass@^3.1.0, minipass@^3.1.1, minipass@^3.1.3: + version "3.1.6" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.6.tgz#3b8150aa688a711a1521af5e8779c1d3bb4f45ee" + integrity sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ== + dependencies: + yallist "^4.0.0" + +minizlib@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d" + integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q== + dependencies: + minipass "^2.9.0" + +minizlib@^2.0.0, minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + +mkdirp-infer-owner@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mkdirp-infer-owner/-/mkdirp-infer-owner-2.0.0.tgz#55d3b368e7d89065c38f32fd38e638f0ab61d316" + integrity sha512-sdqtiFt3lkOaYvTXSRIUjkIdPTcxgv5+fgqYE/5qgwdw12cOrAuzzgzvVExIkH/ul1oeHN3bCLOWSG3XOqbKKw== + dependencies: + chownr "^2.0.0" + infer-owner "^1.0.4" + mkdirp "^1.0.3" + +mkdirp@^0.5.1, mkdirp@^0.5.5: + version "0.5.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== + dependencies: + minimist "^1.2.5" + +mkdirp@^1.0.3, mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + +mnemonist@0.38.3: + version "0.38.3" + resolved "https://registry.yarnpkg.com/mnemonist/-/mnemonist-0.38.3.tgz#35ec79c1c1f4357cfda2fe264659c2775ccd7d9d" + integrity sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw== + dependencies: + obliterator "^1.6.1" + +modify-values@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" + integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== + +mri@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" + integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@^2.0.0, ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +multimatch@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-5.0.0.tgz#932b800963cea7a31a033328fa1e0c3a1874dbe6" + integrity sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA== + dependencies: + "@types/minimatch" "^3.0.3" + array-differ "^3.0.0" + array-union "^2.1.0" + arrify "^2.0.1" + minimatch "^3.0.4" + +mute-stream@0.0.8, mute-stream@~0.0.4: + version "0.0.8" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" + integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= + +negotiator@^0.6.2: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +neo-async@^2.6.0: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +netmask@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/netmask/-/netmask-2.0.2.tgz#8b01a07644065d536383835823bc52004ebac5e7" + integrity sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg== + +no-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" + integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== + dependencies: + lower-case "^2.0.2" + tslib "^2.0.3" + +node-fetch@^2.6.1, node-fetch@^2.6.7: + version "2.6.7" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" + integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== + dependencies: + whatwg-url "^5.0.0" + +node-gyp@^5.0.2: + version "5.1.1" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-5.1.1.tgz#eb915f7b631c937d282e33aed44cb7a025f62a3e" + integrity sha512-WH0WKGi+a4i4DUt2mHnvocex/xPLp9pYt5R6M2JdFB7pJ7Z34hveZ4nDTGTiLXCkitA9T8HFZjhinBCiVHYcWw== + dependencies: + env-paths "^2.2.0" + glob "^7.1.4" + graceful-fs "^4.2.2" + mkdirp "^0.5.1" + nopt "^4.0.1" + npmlog "^4.1.2" + request "^2.88.0" + rimraf "^2.6.3" + semver "^5.7.1" + tar "^4.4.12" + which "^1.3.1" + +node-gyp@^7.1.0: + version "7.1.2" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-7.1.2.tgz#21a810aebb187120251c3bcec979af1587b188ae" + integrity sha512-CbpcIo7C3eMu3dL1c3d0xw449fHIGALIJsRP4DDPHpyiW8vcriNY7ubh9TE4zEKfSxscY7PjeFnshE7h75ynjQ== + dependencies: + env-paths "^2.2.0" + glob "^7.1.4" + graceful-fs "^4.2.3" + nopt "^5.0.0" + npmlog "^4.1.2" + request "^2.88.2" + rimraf "^3.0.2" + semver "^7.3.2" + tar "^6.0.2" + which "^2.0.2" + +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= + +node-releases@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.2.tgz#7139fe71e2f4f11b47d4d2986aaf8c48699e0c01" + integrity sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg== + +nopt@^4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.3.tgz#a375cad9d02fd921278d954c2254d5aa57e15e48" + integrity sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg== + dependencies: + abbrev "1" + osenv "^0.1.4" + +nopt@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" + integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== + dependencies: + abbrev "1" + +normalize-package-data@^2.0.0, normalize-package-data@^2.3.2, normalize-package-data@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== + dependencies: + hosted-git-info "^2.1.4" + resolve "^1.10.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-package-data@^3.0.0, normalize-package-data@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-3.0.3.tgz#dbcc3e2da59509a0983422884cd172eefdfa525e" + integrity sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA== + dependencies: + hosted-git-info "^4.0.1" + is-core-module "^2.5.0" + semver "^7.3.4" + validate-npm-package-license "^3.0.1" + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +normalize-url@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" + integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== + +npm-bundled@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.1.2.tgz#944c78789bd739035b70baa2ca5cc32b8d860bc1" + integrity sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ== + dependencies: + npm-normalize-package-bin "^1.0.1" + +npm-install-checks@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/npm-install-checks/-/npm-install-checks-4.0.0.tgz#a37facc763a2fde0497ef2c6d0ac7c3fbe00d7b4" + integrity sha512-09OmyDkNLYwqKPOnbI8exiOZU2GVVmQp7tgez2BPi5OZC8M82elDAps7sxC4l//uSUtotWqoEIDwjRvWH4qz8w== + dependencies: + semver "^7.1.1" + +npm-lifecycle@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/npm-lifecycle/-/npm-lifecycle-3.1.5.tgz#9882d3642b8c82c815782a12e6a1bfeed0026309" + integrity sha512-lDLVkjfZmvmfvpvBzA4vzee9cn+Me4orq0QF8glbswJVEbIcSNWib7qGOffolysc3teCqbbPZZkzbr3GQZTL1g== + dependencies: + byline "^5.0.0" + graceful-fs "^4.1.15" + node-gyp "^5.0.2" + resolve-from "^4.0.0" + slide "^1.1.6" + uid-number "0.0.6" + umask "^1.1.0" + which "^1.3.1" + +npm-normalize-package-bin@^1.0.0, npm-normalize-package-bin@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz#6e79a41f23fd235c0623218228da7d9c23b8f6e2" + integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA== + +npm-package-arg@^8.0.0, npm-package-arg@^8.0.1, npm-package-arg@^8.1.0, npm-package-arg@^8.1.2, npm-package-arg@^8.1.5: + version "8.1.5" + resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-8.1.5.tgz#3369b2d5fe8fdc674baa7f1786514ddc15466e44" + integrity sha512-LhgZrg0n0VgvzVdSm1oiZworPbTxYHUJCgtsJW8mGvlDpxTM1vSJc3m5QZeUkhAHIzbz3VCHd/R4osi1L1Tg/Q== + dependencies: + hosted-git-info "^4.0.1" + semver "^7.3.4" + validate-npm-package-name "^3.0.0" + +npm-packlist@^2.1.4: + version "2.2.2" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-2.2.2.tgz#076b97293fa620f632833186a7a8f65aaa6148c8" + integrity sha512-Jt01acDvJRhJGthnUJVF/w6gumWOZxO7IkpY/lsX9//zqQgnF7OJaxgQXcerd4uQOLu7W5bkb4mChL9mdfm+Zg== + dependencies: + glob "^7.1.6" + ignore-walk "^3.0.3" + npm-bundled "^1.1.1" + npm-normalize-package-bin "^1.0.1" + +npm-pick-manifest@^6.0.0, npm-pick-manifest@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-6.1.1.tgz#7b5484ca2c908565f43b7f27644f36bb816f5148" + integrity sha512-dBsdBtORT84S8V8UTad1WlUyKIY9iMsAmqxHbLdeEeBNMLQDlDWWra3wYUx9EBEIiG/YwAy0XyNHDd2goAsfuA== + dependencies: + npm-install-checks "^4.0.0" + npm-normalize-package-bin "^1.0.1" + npm-package-arg "^8.1.2" + semver "^7.3.4" + +npm-registry-fetch@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/npm-registry-fetch/-/npm-registry-fetch-11.0.0.tgz#68c1bb810c46542760d62a6a965f85a702d43a76" + integrity sha512-jmlgSxoDNuhAtxUIG6pVwwtz840i994dL14FoNVZisrmZW5kWd63IUTNv1m/hyRSGSqWjCUp/YZlS1BJyNp9XA== + dependencies: + make-fetch-happen "^9.0.1" + minipass "^3.1.3" + minipass-fetch "^1.3.0" + minipass-json-stream "^1.0.1" + minizlib "^2.0.0" + npm-package-arg "^8.0.0" + +npm-registry-fetch@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/npm-registry-fetch/-/npm-registry-fetch-9.0.0.tgz#86f3feb4ce00313bc0b8f1f8f69daae6face1661" + integrity sha512-PuFYYtnQ8IyVl6ib9d3PepeehcUeHN9IO5N/iCRhyg9tStQcqGQBRVHmfmMWPDERU3KwZoHFvbJ4FPXPspvzbA== + dependencies: + "@npmcli/ci-detect" "^1.0.0" + lru-cache "^6.0.0" + make-fetch-happen "^8.0.9" + minipass "^3.1.3" + minipass-fetch "^1.3.0" + minipass-json-stream "^1.0.1" + minizlib "^2.0.0" + npm-package-arg "^8.0.0" + +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +npmlog@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= + +nwsapi@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" + integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ== + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + +object-assign@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +object-inspect@^1.11.0, object-inspect@^1.9.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0" + integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g== + +object-is@^1.1.4: + version "1.1.5" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" + integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + +object-keys@^1.0.12, object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" + integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + has-symbols "^1.0.1" + object-keys "^1.1.1" + +object.getownpropertydescriptors@^2.0.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.3.tgz#b223cf38e17fefb97a63c10c91df72ccb386df9e" + integrity sha512-VdDoCwvJI4QdC6ndjpqFmoL3/+HxffFBbcJzKi5hwLLqqx3mdbedRpfZDdK0SrOSauj8X4GzBvnDZl4vTN7dOw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.1" + +object.values@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.5.tgz#959f63e3ce9ef108720333082131e4a459b716ac" + integrity sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.1" + +obliterator@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/obliterator/-/obliterator-1.6.1.tgz#dea03e8ab821f6c4d96a299e17aef6a3af994ef3" + integrity sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig== + +once@^1.3.0, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +one-time@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/one-time/-/one-time-1.0.0.tgz#e06bc174aed214ed58edede573b433bbf827cb45" + integrity sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g== + dependencies: + fn.name "1.x.x" + +onetime@^5.1.0, onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +oo-ascii-tree@^1.55.1: + version "1.55.1" + resolved "https://registry.yarnpkg.com/oo-ascii-tree/-/oo-ascii-tree-1.55.1.tgz#111b6dd8e2ec8f7068e3e89e687dc29e048880e8" + integrity sha512-wGtYFm45kmxdss2XrdXC14uDUfyekbaqqZJrfvPtYHSa98Bk+RXHdTHHLQ1kwem6HT5c3ogf7+ZUBhX0B034iA== + +optionator@^0.8.1: + version "0.8.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" + integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.6" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + word-wrap "~1.2.3" + +optionator@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" + integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.3" + +os-homedir@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= + +os-tmpdir@^1.0.0, os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= + +osenv@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" + integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.0" + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= + +p-limit@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" + integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== + dependencies: + p-try "^1.0.0" + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-locate@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= + dependencies: + p-limit "^1.1.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-map-series@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-map-series/-/p-map-series-2.1.0.tgz#7560d4c452d9da0c07e692fdbfe6e2c81a2a91f2" + integrity sha512-RpYIIK1zXSNEOdwxcfe7FdvGcs7+y5n8rifMhMNWvaxRNMPINJHF5GDeuVxWqnfrcHPSCnp7Oo5yNXHId9Av2Q== + +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + +p-pipe@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-pipe/-/p-pipe-3.1.0.tgz#48b57c922aa2e1af6a6404cb7c6bf0eb9cc8e60e" + integrity sha512-08pj8ATpzMR0Y80x50yJHn37NF6vjrqHutASaX5LiH5npS9XPvrUmscd9MF5R4fuYRHOxQR1FfMIlF7AzwoPqw== + +p-queue@^6.6.2: + version "6.6.2" + resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-6.6.2.tgz#2068a9dcf8e67dd0ec3e7a2bcb76810faa85e426" + integrity sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ== + dependencies: + eventemitter3 "^4.0.4" + p-timeout "^3.2.0" + +p-reduce@^2.0.0, p-reduce@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-2.1.0.tgz#09408da49507c6c274faa31f28df334bc712b64a" + integrity sha512-2USApvnsutq8uoxZBGbbWM0JIYLiEMJ9RlaN7fAzVNb9OZN0SHjjTTfIcb667XynS5Y1VhwDJVDa72TnPzAYWw== + +p-timeout@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" + integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg== + dependencies: + p-finally "^1.0.0" + +p-try@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" + integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +p-waterfall@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/p-waterfall/-/p-waterfall-2.1.1.tgz#63153a774f472ccdc4eb281cdb2967fcf158b2ee" + integrity sha512-RRTnDb2TBG/epPRI2yYXsimO0v3BXC8Yd3ogr1545IaqKK17VGhbWVeGGN+XfCm/08OK8635nH31c8bATkHuSw== + dependencies: + p-reduce "^2.0.0" + +pac-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/pac-proxy-agent/-/pac-proxy-agent-5.0.0.tgz#b718f76475a6a5415c2efbe256c1c971c84f635e" + integrity sha512-CcFG3ZtnxO8McDigozwE3AqAw15zDvGH+OjXO4kzf7IkEKkQ4gxQ+3sdF50WmhQ4P/bVusXcqNE2S3XrNURwzQ== + dependencies: + "@tootallnate/once" "1" + agent-base "6" + debug "4" + get-uri "3" + http-proxy-agent "^4.0.1" + https-proxy-agent "5" + pac-resolver "^5.0.0" + raw-body "^2.2.0" + socks-proxy-agent "5" + +pac-resolver@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/pac-resolver/-/pac-resolver-5.0.0.tgz#1d717a127b3d7a9407a16d6e1b012b13b9ba8dc0" + integrity sha512-H+/A6KitiHNNW+bxBKREk2MCGSxljfqRX76NjummWEYIat7ldVXRU3dhRIE3iXZ0nvGBk6smv3nntxKkzRL8NA== + dependencies: + degenerator "^3.0.1" + ip "^1.1.5" + netmask "^2.0.1" + +pacote@^11.2.6: + version "11.3.5" + resolved "https://registry.yarnpkg.com/pacote/-/pacote-11.3.5.tgz#73cf1fc3772b533f575e39efa96c50be8c3dc9d2" + integrity sha512-fT375Yczn4zi+6Hkk2TBe1x1sP8FgFsEIZ2/iWaXY2r/NkhDJfxbcn5paz1+RTFCyNf+dPnaoBDJoAxXSU8Bkg== + dependencies: + "@npmcli/git" "^2.1.0" + "@npmcli/installed-package-contents" "^1.0.6" + "@npmcli/promise-spawn" "^1.2.0" + "@npmcli/run-script" "^1.8.2" + cacache "^15.0.5" + chownr "^2.0.0" + fs-minipass "^2.1.0" + infer-owner "^1.0.4" + minipass "^3.1.3" + mkdirp "^1.0.3" + npm-package-arg "^8.0.1" + npm-packlist "^2.1.4" + npm-pick-manifest "^6.0.0" + npm-registry-fetch "^11.0.0" + promise-retry "^2.0.1" + read-package-json-fast "^2.0.1" + rimraf "^3.0.2" + ssri "^8.0.1" + tar "^6.1.0" + +param-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5" + integrity sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A== + dependencies: + dot-case "^3.0.4" + tslib "^2.0.3" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-json@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" + integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= + dependencies: + error-ex "^1.3.1" + json-parse-better-errors "^1.0.1" + +parse-json@^5.0.0, parse-json@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +parse-path@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/parse-path/-/parse-path-4.0.3.tgz#82d81ec3e071dcc4ab49aa9f2c9c0b8966bb22bf" + integrity sha512-9Cepbp2asKnWTJ9x2kpw6Fe8y9JDbqwahGCTvklzd/cEq5C5JC59x2Xb0Kx+x0QZ8bvNquGO8/BWP0cwBHzSAA== + dependencies: + is-ssh "^1.3.0" + protocols "^1.4.0" + qs "^6.9.4" + query-string "^6.13.8" + +parse-url@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/parse-url/-/parse-url-6.0.0.tgz#f5dd262a7de9ec00914939220410b66cff09107d" + integrity sha512-cYyojeX7yIIwuJzledIHeLUBVJ6COVLeT4eF+2P6aKVzwvgKQPndCBv3+yQ7pcWjqToYwaligxzSYNNmGoMAvw== + dependencies: + is-ssh "^1.3.0" + normalize-url "^6.1.0" + parse-path "^4.0.0" + protocols "^1.4.0" + +parse5@6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" + integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== + +pascal-case@3.1.2, pascal-case@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.2.tgz#b48e0ef2b98e205e7c1dae747d0b1508237660eb" + integrity sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + +path-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/path-case/-/path-case-3.0.4.tgz#9168645334eb942658375c56f80b4c0cb5f82c6f" + integrity sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg== + dependencies: + dot-case "^3.0.4" + tslib "^2.0.3" + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-type@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" + integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg== + dependencies: + pify "^3.0.0" + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pify@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= + +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= + +pify@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" + integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== + +pify@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-5.0.0.tgz#1f5eca3f5e87ebec28cc6d54a0e4aaf00acc127f" + integrity sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA== + +pirates@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" + integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== + +pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= + +prettier-linter-helpers@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" + integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== + dependencies: + fast-diff "^1.1.2" + +prettier@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.1.tgz#fff75fa9d519c54cf0fce328c1017d94546bc56a" + integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg== + +pretty-format@^27.0.0, pretty-format@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" + integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== + dependencies: + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" + react-is "^17.0.1" + +printj@~1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/printj/-/printj-1.3.1.tgz#9af6b1d55647a1587ac44f4c1654a4b95b8e12cb" + integrity sha512-GA3TdL8szPK4AQ2YnOe/b+Y1jUFwmmGMMK/qbY7VcE3Z7FU8JstbKiKRzO6CIiAKPhTO8m01NoQ0V5f3jc4OGg== + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +progress@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + +promise-inflight@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" + integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= + +promise-retry@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22" + integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g== + dependencies: + err-code "^2.0.2" + retry "^0.12.0" + +promptly@3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/promptly/-/promptly-3.0.2.tgz#5f628f755f079b653dbaeae2e64ceea9168e6844" + integrity sha512-Ox2VTTZvDgcgjYrzXdX2Czc/i7W2TpxI2WESpvki5LfZrYoAWEcXfLhjaejluZMYjdkMoeCxiXFEqYhbRjALdw== + dependencies: + read "^1.0.4" + strip-ansi "^4.0.0" + +prompts@^2.0.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" + integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.5" + +promzard@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/promzard/-/promzard-0.3.0.tgz#26a5d6ee8c7dee4cb12208305acfb93ba382a9ee" + integrity sha1-JqXW7ox97kyxIggwWs+5O6OCqe4= + dependencies: + read "1" + +proto-list@~1.2.1: + version "1.2.4" + resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" + integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk= + +protocols@^1.1.0, protocols@^1.4.0: + version "1.4.8" + resolved "https://registry.yarnpkg.com/protocols/-/protocols-1.4.8.tgz#48eea2d8f58d9644a4a32caae5d5db290a075ce8" + integrity sha512-IgjKyaUSjsROSO8/D49Ab7hP8mJgTYcqApOqdPhLoPxAplXmkp+zRvsrSQjFn5by0rhm4VH0GAUELIPpx7B1yg== + +proxy-agent@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/proxy-agent/-/proxy-agent-5.0.0.tgz#d31405c10d6e8431fde96cba7a0c027ce01d633b" + integrity sha512-gkH7BkvLVkSfX9Dk27W6TyNOWWZWRilRfk1XxGNWOYJ2TuedAv1yFpCaU9QSBmBe716XOTNpYNOzhysyw8xn7g== + dependencies: + agent-base "^6.0.0" + debug "4" + http-proxy-agent "^4.0.0" + https-proxy-agent "^5.0.0" + lru-cache "^5.1.1" + pac-proxy-agent "^5.0.0" + proxy-from-env "^1.0.0" + socks-proxy-agent "^5.0.0" + +proxy-from-env@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + +psl@^1.1.28, psl@^1.1.33: + version "1.8.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" + integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== + +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= + +punycode@^2.1.0, punycode@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +q@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" + integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= + +qs@^6.9.4: + version "6.10.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e" + integrity sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ== + dependencies: + side-channel "^1.0.4" + +qs@~6.5.2: + version "6.5.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" + integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== + +query-string@^6.13.8: + version "6.14.1" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.14.1.tgz#7ac2dca46da7f309449ba0f86b1fd28255b0c86a" + integrity sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw== + dependencies: + decode-uri-component "^0.2.0" + filter-obj "^1.1.0" + split-on-first "^1.0.0" + strict-uri-encode "^2.0.0" + +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +quick-lru@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" + integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== + +raw-body@^2.2.0: + version "2.5.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" + integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +react-is@^17.0.1: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + +read-cmd-shim@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-cmd-shim/-/read-cmd-shim-2.0.0.tgz#4a50a71d6f0965364938e9038476f7eede3928d9" + integrity sha512-HJpV9bQpkl6KwjxlJcBoqu9Ba0PQg8TqSNIOrulGt54a0uup0HtevreFHzYzkm0lpnleRdNBzXznKrgxglEHQw== + +read-package-json-fast@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/read-package-json-fast/-/read-package-json-fast-2.0.3.tgz#323ca529630da82cb34b36cc0b996693c98c2b83" + integrity sha512-W/BKtbL+dUjTuRL2vziuYhp76s5HZ9qQhd/dKfWIZveD0O40453QNyZhC0e63lqZrAQ4jiOapVoeJ7JrszenQQ== + dependencies: + json-parse-even-better-errors "^2.3.0" + npm-normalize-package-bin "^1.0.1" + +read-package-json@^2.0.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/read-package-json/-/read-package-json-2.1.2.tgz#6992b2b66c7177259feb8eaac73c3acd28b9222a" + integrity sha512-D1KmuLQr6ZSJS0tW8hf3WGpRlwszJOXZ3E8Yd/DNRaM5d+1wVRZdHlpGBLAuovjr28LbWvjpWkBHMxpRGGjzNA== + dependencies: + glob "^7.1.1" + json-parse-even-better-errors "^2.3.0" + normalize-package-data "^2.0.0" + npm-normalize-package-bin "^1.0.0" + +read-package-json@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/read-package-json/-/read-package-json-3.0.1.tgz#c7108f0b9390257b08c21e3004d2404c806744b9" + integrity sha512-aLcPqxovhJTVJcsnROuuzQvv6oziQx4zd3JvG0vGCL5MjTONUc4uJ90zCBC6R7W7oUKBNoR/F8pkyfVwlbxqng== + dependencies: + glob "^7.1.1" + json-parse-even-better-errors "^2.3.0" + normalize-package-data "^3.0.0" + npm-normalize-package-bin "^1.0.0" + +read-package-json@^4.1.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/read-package-json/-/read-package-json-4.1.2.tgz#b444d047de7c75d4a160cb056d00c0693c1df703" + integrity sha512-Dqer4pqzamDE2O4M55xp1qZMuLPqi4ldk2ya648FOMHRjwMzFhuxVrG04wd0c38IsvkVdr3vgHI6z+QTPdAjrQ== + dependencies: + glob "^7.1.1" + json-parse-even-better-errors "^2.3.0" + normalize-package-data "^3.0.0" + npm-normalize-package-bin "^1.0.0" + +read-package-tree@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/read-package-tree/-/read-package-tree-5.3.1.tgz#a32cb64c7f31eb8a6f31ef06f9cedf74068fe636" + integrity sha512-mLUDsD5JVtlZxjSlPPx1RETkNjjvQYuweKwNVt1Sn8kP5Jh44pvYuUHCp6xSVDZWbNxVxG5lyZJ921aJH61sTw== + dependencies: + read-package-json "^2.0.0" + readdir-scoped-modules "^1.0.0" + util-promisify "^2.1.0" + +read-pkg-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07" + integrity sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc= + dependencies: + find-up "^2.0.0" + read-pkg "^3.0.0" + +read-pkg-up@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" + integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg== + dependencies: + find-up "^4.1.0" + read-pkg "^5.2.0" + type-fest "^0.8.1" + +read-pkg@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" + integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k= + dependencies: + load-json-file "^4.0.0" + normalize-package-data "^2.3.2" + path-type "^3.0.0" + +read-pkg@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc" + integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg== + dependencies: + "@types/normalize-package-data" "^2.4.0" + normalize-package-data "^2.5.0" + parse-json "^5.0.0" + type-fest "^0.6.0" + +read@1, read@^1.0.4, read@~1.0.1: + version "1.0.7" + resolved "https://registry.yarnpkg.com/read/-/read-1.0.7.tgz#b3da19bd052431a97671d44a42634adf710b40c4" + integrity sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ= + dependencies: + mute-stream "~0.0.4" + +readable-stream@1.1.x: + version "1.1.14" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" + integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk= + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readable-stream@^2.0.0, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@~2.3.6: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readdir-glob@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/readdir-glob/-/readdir-glob-1.1.1.tgz#f0e10bb7bf7bfa7e0add8baffdc54c3f7dbee6c4" + integrity sha512-91/k1EzZwDx6HbERR+zucygRFfiPl2zkIYZtv3Jjr6Mn7SkKcVct8aVO+sSRiGMc6fLf72du3d92/uY63YPdEA== + dependencies: + minimatch "^3.0.4" + +readdir-scoped-modules@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz#8d45407b4f870a0dcaebc0e28670d18e74514309" + integrity sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw== + dependencies: + debuglog "^1.0.1" + dezalgo "^1.0.0" + graceful-fs "^4.1.2" + once "^1.3.0" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +redent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" + integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== + dependencies: + indent-string "^4.0.0" + strip-indent "^3.0.0" + +regexp.prototype.flags@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.1.tgz#b3f4c0059af9e47eca9f3f660e51d81307e72307" + integrity sha512-pMR7hBVUUGI7PMA37m2ofIdQCsomVnas+Jn5UPGAHQ+/LlwKm/aTLJHdasmHRzlfeZwHiAOaRSo2rbBDm3nNUQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + +regexpp@^3.0.0, regexpp@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" + integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== + +request@^2.88.0, request@^2.88.2: + version "2.88.2" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" + integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.3" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.5.0" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +requireindex@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/requireindex/-/requireindex-1.2.0.tgz#3463cdb22ee151902635aa6c9535d4de9c2ef1ef" + integrity sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww== + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve.exports@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.1.0.tgz#5ce842b94b05146c0e03076985d1d0e7e48c90c9" + integrity sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ== + +resolve@^1.10.0, resolve@^1.10.1, resolve@^1.20.0: + version "1.22.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198" + integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw== + dependencies: + is-core-module "^2.8.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + +retry@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" + integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rfdc@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" + integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== + +rimraf@^2.6.3: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + +rimraf@^3.0.0, rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +run-async@^2.4.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" + integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +rxjs@^6.6.0: + version "6.6.7" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" + integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== + dependencies: + tslib "^1.9.0" + +rxjs@^7.5.1: + version "7.5.5" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.5.tgz#2ebad89af0f560f460ad5cc4213219e1f7dd4e9f" + integrity sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw== + dependencies: + tslib "^2.1.0" + +safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@^5.2.1, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-stable-stringify@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz#ab67cbe1fe7d40603ca641c5e765cb942d04fc73" + integrity sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg== + +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +sax@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" + integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o= + +sax@>=0.6.0: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== + +saxes@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d" + integrity sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw== + dependencies: + xmlchars "^2.2.0" + +semver-intersect@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/semver-intersect/-/semver-intersect-1.4.0.tgz#bdd9c06bedcdd2fedb8cd352c3c43ee8c61321f3" + integrity sha512-d8fvGg5ycKAq0+I6nfWeCx6ffaWJCsBYU0H2Rq56+/zFePYfT8mXkB3tWBSjR5BerkHNZ5eTPIk1/LBYas35xQ== + dependencies: + semver "^5.0.0" + +"semver@2 || 3 || 4 || 5", semver@^5.0.0, semver@^5.6.0, semver@^5.7.1: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + +semver@7.3.5, semver@7.x, semver@^7.1.1, semver@^7.1.3, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5: + version "7.3.5" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" + integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== + dependencies: + lru-cache "^6.0.0" + +semver@^6.0.0, semver@^6.1.0, semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +sentence-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/sentence-case/-/sentence-case-3.0.4.tgz#3645a7b8c117c787fde8702056225bb62a45131f" + integrity sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + upper-case-first "^2.0.2" + +set-blocking@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shiki@^0.10.0: + version "0.10.1" + resolved "https://registry.yarnpkg.com/shiki/-/shiki-0.10.1.tgz#6f9a16205a823b56c072d0f1a0bcd0f2646bef14" + integrity sha512-VsY7QJVzU51j5o1+DguUd+6vmCmZ5v/6gYu4vyYAhzjuNQU6P/vmSy4uQaOhvje031qQMiW0d2BwgMH52vqMng== + dependencies: + jsonc-parser "^3.0.0" + vscode-oniguruma "^1.6.1" + vscode-textmate "5.2.0" + +side-channel@^1.0.3, side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + +signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +simple-swizzle@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo= + dependencies: + is-arrayish "^0.3.1" + +sisteransi@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" + integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +slice-ansi@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" + integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + +slice-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" + integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + +slice-ansi@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-5.0.0.tgz#b73063c57aa96f9cd881654b15294d95d285c42a" + integrity sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ== + dependencies: + ansi-styles "^6.0.0" + is-fullwidth-code-point "^4.0.0" + +slide@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" + integrity sha1-VusCfWW00tzmyy4tMsTUr8nh1wc= + +smart-buffer@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" + integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== + +snake-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c" + integrity sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg== + dependencies: + dot-case "^3.0.4" + tslib "^2.0.3" + +socks-proxy-agent@5, socks-proxy-agent@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-5.0.1.tgz#032fb583048a29ebffec2e6a73fca0761f48177e" + integrity sha512-vZdmnjb9a2Tz6WEQVIurybSwElwPxMZaIc7PzqbJTrezcKNznv6giT7J7tZDZ1BojVaa1jvO/UiUdhDVB0ACoQ== + dependencies: + agent-base "^6.0.2" + debug "4" + socks "^2.3.3" + +socks-proxy-agent@^6.0.0: + version "6.1.1" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-6.1.1.tgz#e664e8f1aaf4e1fb3df945f09e3d94f911137f87" + integrity sha512-t8J0kG3csjA4g6FTbsMOWws+7R7vuRC8aQ/wy3/1OWmsgwA68zs/+cExQ0koSitUDXqhufF/YJr9wtNMZHw5Ew== + dependencies: + agent-base "^6.0.2" + debug "^4.3.1" + socks "^2.6.1" + +socks@^2.3.3, socks@^2.6.1: + version "2.6.2" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.6.2.tgz#ec042d7960073d40d94268ff3bb727dc685f111a" + integrity sha512-zDZhHhZRY9PxRruRMR7kMhnf3I8hDs4S3f9RecfnGxvcBHQcKcIH/oUcEWffsfl1XxdYlA7nnlGbbTvPz9D8gA== + dependencies: + ip "^1.1.5" + smart-buffer "^4.2.0" + +sort-json@^2.0.0, sort-json@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/sort-json/-/sort-json-2.0.1.tgz#7338783bef807185dc37d5b02e3afd905d537cfb" + integrity sha512-s8cs2bcsQCzo/P2T/uoU6Js4dS/jnX8+4xunziNoq9qmSpZNCrRIAIvp4avsz0ST18HycV4z/7myJ7jsHWB2XQ== + dependencies: + detect-indent "^5.0.0" + detect-newline "^2.1.0" + minimist "^1.2.0" + +sort-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-2.0.0.tgz#658535584861ec97d730d6cf41822e1f56684128" + integrity sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg= + dependencies: + is-plain-obj "^1.0.0" + +sort-keys@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-4.2.0.tgz#6b7638cee42c506fff8c1cecde7376d21315be18" + integrity sha512-aUYIEU/UviqPgc8mHR6IW1EGxkAXpeRETYcrzg8cLAvUPZcpAlleSXHV2mY7G12GphSH6Gzv+4MMVSSkbdteHg== + dependencies: + is-plain-obj "^2.0.0" + +source-map-support@^0.5.6: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.5.0: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= + +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +source-map@^0.7.3: + version "0.7.3" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" + integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== + +spdx-correct@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" + integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" + integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== + +spdx-expression-parse@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" + integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.11" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz#50c0d8c40a14ec1bf449bae69a0ea4685a9d9f95" + integrity sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g== + +spdx-license-list@^6.4.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/spdx-license-list/-/spdx-license-list-6.4.0.tgz#9850c3699c1d35745285607d064d2a5145049d12" + integrity sha512-4BxgJ1IZxTJuX1YxMGu2cRYK46Bk9zJNTK2/R0wNZR0cm+6SVl26/uG7FQmQtxoJQX1uZ0EpTi2L7zvMLboaBA== + +split-on-first@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f" + integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw== + +split2@^3.0.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/split2/-/split2-3.2.2.tgz#bf2cf2a37d838312c249c89206fd7a17dd12365f" + integrity sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg== + dependencies: + readable-stream "^3.0.0" + +split@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" + integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg== + dependencies: + through "2" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + +sshpk@^1.7.0: + version "1.17.0" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.17.0.tgz#578082d92d4fe612b13007496e543fa0fbcbe4c5" + integrity sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + +ssri@^8.0.0, ssri@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af" + integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ== + dependencies: + minipass "^3.1.1" + +stack-trace@0.0.x: + version "0.0.10" + resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" + integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA= + +stack-utils@^2.0.3: + version "2.0.5" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.5.tgz#d25265fca995154659dbbfba3b49254778d2fdd5" + integrity sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA== + dependencies: + escape-string-regexp "^2.0.0" + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +streamroller@^3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-3.0.6.tgz#52823415800ded79a49aa3f7712f50a422b97493" + integrity sha512-Qz32plKq/MZywYyhEatxyYc8vs994Gz0Hu2MSYXXLD233UyPeIeRBZARIIGwFer4Mdb8r3Y2UqKkgyDghM6QCg== + dependencies: + date-format "^4.0.6" + debug "^4.3.4" + fs-extra "^10.0.1" + +strict-uri-encode@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" + integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY= + +string-argv@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" + integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg== + +string-length@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" + integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== + dependencies: + char-regex "^1.0.2" + strip-ansi "^6.0.0" + +string-width@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^5.0.0: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + +string.prototype.repeat@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/string.prototype.repeat/-/string.prototype.repeat-0.2.0.tgz#aba36de08dcee6a5a337d49b2ea1da1b28fc0ecf" + integrity sha1-q6Nt4I3O5qWjN9SbLqHaGyj8Ds8= + +string.prototype.trimend@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80" + integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + +string.prototype.trimstart@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed" + integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= + dependencies: + ansi-regex "^3.0.0" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2" + integrity sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw== + dependencies: + ansi-regex "^6.0.1" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= + +strip-bom@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" + integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +strip-indent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" + integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== + dependencies: + min-indent "^1.0.0" + +strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +strong-log-transformer@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/strong-log-transformer/-/strong-log-transformer-2.1.0.tgz#0f5ed78d325e0421ac6f90f7f10e691d6ae3ae10" + integrity sha512-B3Hgul+z0L9a236FAUC9iZsL+nVHgoCJnqCbN588DjYxvGXaXaaFbfmQ/JhvKjZwsOukuR72XbHv71Qkug0HxA== + dependencies: + duplexer "^0.1.1" + minimist "^1.2.0" + through "^2.3.4" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.0.0, supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-color@^9.0.2: + version "9.2.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-9.2.1.tgz#599dc9d45acf74c6176e0d880bab1d7d718fe891" + integrity sha512-Obv7ycoCTG51N7y175StI9BlAXrmgZrFhZOb0/PyjHBher/NmsdBgbbQ1Inhq+gIhz6+7Gb+jWF2Vqi7Mf1xnQ== + +supports-hyperlinks@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz#4f77b42488765891774b70c79babd87f9bd594bb" + integrity sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ== + dependencies: + has-flag "^4.0.0" + supports-color "^7.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +symbol-tree@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" + integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== + +"t@file:packages/@aws-accelerator/config/lib/common-types": + version "0.0.0" + +table@^6.8.0: + version "6.8.0" + resolved "https://registry.yarnpkg.com/table/-/table-6.8.0.tgz#87e28f14fa4321c3377ba286f07b79b281a3b3ca" + integrity sha512-s/fitrbVeEyHKFa7mFdkuQMWlH1Wgw/yEXMt5xACT4ZpzWFluehAxRtUUQKPuWhaLAWhFcVx6w3oC8VKaUfPGA== + dependencies: + ajv "^8.0.1" + lodash.truncate "^4.4.2" + slice-ansi "^4.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" + +tar-stream@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" + integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== + dependencies: + bl "^4.0.3" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + +tar@^4.4.12: + version "4.4.19" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.19.tgz#2e4d7263df26f2b914dee10c825ab132123742f3" + integrity sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA== + dependencies: + chownr "^1.1.4" + fs-minipass "^1.2.7" + minipass "^2.9.0" + minizlib "^1.3.3" + mkdirp "^0.5.5" + safe-buffer "^5.2.1" + yallist "^3.1.1" + +tar@^6.0.2, tar@^6.1.0: + version "6.1.11" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" + integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^3.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + +temp-dir@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-1.0.0.tgz#0a7c0ea26d3a39afa7e0ebea9c1fc0bc4daa011d" + integrity sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0= + +temp-dir@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-2.0.0.tgz#bde92b05bdfeb1516e804c9c00ad45177f31321e" + integrity sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg== + +temp-write@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/temp-write/-/temp-write-4.0.0.tgz#cd2e0825fc826ae72d201dc26eef3bf7e6fc9320" + integrity sha512-HIeWmj77uOOHb0QX7siN3OtwV3CTntquin6TNVg6SHOqCP3hYKmox90eeFOGaY1MqJ9WYDDjkyZrW6qS5AWpbw== + dependencies: + graceful-fs "^4.1.15" + is-stream "^2.0.0" + make-dir "^3.0.0" + temp-dir "^1.0.0" + uuid "^3.3.2" + +tempy@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/tempy/-/tempy-2.0.0.tgz#c434b89486e112151d5e91ef25592c8ee6fec4f6" + integrity sha512-m+QReZVhpa0Y56fmfoLFRZN4aDFdd3qVd8a9k3RfyTw/1utVYNg+Ar4BY6l4/TlkhYCCJFfhYWt9uy0127buJg== + dependencies: + del "^6.0.0" + is-stream "^3.0.0" + temp-dir "^2.0.0" + type-fest "^2.0.0" + unique-string "^3.0.0" + +terminal-link@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994" + integrity sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ== + dependencies: + ansi-escapes "^4.2.1" + supports-hyperlinks "^2.0.0" + +test-exclude@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" + integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== + dependencies: + "@istanbuljs/schema" "^0.1.2" + glob "^7.1.4" + minimatch "^3.0.4" + +text-extensions@^1.0.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/text-extensions/-/text-extensions-1.9.0.tgz#1853e45fee39c945ce6f6c36b2d659b5aabc2a26" + integrity sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ== + +text-hex@1.0.x: + version "1.0.0" + resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" + integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= + +throat@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/throat/-/throat-6.0.1.tgz#d514fedad95740c12c2d7fc70ea863eb51ade375" + integrity sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w== + +through2@^2.0.0: + version "2.0.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" + integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== + dependencies: + readable-stream "~2.3.6" + xtend "~4.0.1" + +through2@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/through2/-/through2-4.0.2.tgz#a7ce3ac2a7a8b0b966c80e7c49f0484c3b239764" + integrity sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw== + dependencies: + readable-stream "3" + +through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6, through@^2.3.8: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + +tmpl@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" + integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +tough-cookie@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.0.0.tgz#d822234eeca882f991f0f908824ad2622ddbece4" + integrity sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg== + dependencies: + psl "^1.1.33" + punycode "^2.1.1" + universalify "^0.1.2" + +tough-cookie@~2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.1" + +tr46@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.1.0.tgz#fa87aa81ca5d5941da8cbf1f9b749dc969a4e240" + integrity sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw== + dependencies: + punycode "^2.1.1" + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= + +trim-newlines@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" + integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw== + +triple-beam@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9" + integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw== + +ts-jest@27.1.1: + version "27.1.1" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-27.1.1.tgz#5a54aca96db1dac37c681f3029dd10f3a8c36192" + integrity sha512-Ds0VkB+cB+8g2JUmP/GKWndeZcCKrbe6jzolGrVWdqVUFByY/2KDHqxJ7yBSon7hDB1TA4PXxjfZ+JjzJisvgA== + dependencies: + bs-logger "0.x" + fast-json-stable-stringify "2.x" + jest-util "^27.0.0" + json5 "2.x" + lodash.memoize "4.x" + make-error "1.x" + semver "7.x" + yargs-parser "20.x" + +ts-node@10.4.0: + version "10.4.0" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.4.0.tgz#680f88945885f4e6cf450e7f0d6223dd404895f7" + integrity sha512-g0FlPvvCXSIO1JDF6S232P5jPYqBkRL9qly81ZgAOSU7rwI0stphCgd2kLiCrU9DjQCrJMWEqcNSjQL02s6d8A== + dependencies: + "@cspotcode/source-map-support" "0.7.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + yn "3.1.1" + +ts-node@10.7.0: + version "10.7.0" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.7.0.tgz#35d503d0fab3e2baa672a0e94f4b40653c2463f5" + integrity sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A== + dependencies: + "@cspotcode/source-map-support" "0.7.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.0" + yn "3.1.1" + +tsconfig-paths@^3.11.0, tsconfig-paths@^3.9.0: + version "3.14.1" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a" + integrity sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.1" + minimist "^1.2.6" + strip-bom "^3.0.0" + +tslib@^1.11.1, tslib@^1.8.1, tslib@^1.9.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" + integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== + +tsutils@^3.21.0: + version "3.21.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" + integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== + dependencies: + tslib "^1.8.1" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= + dependencies: + prelude-ls "~1.1.2" + +type-detect@4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + +type-fest@^0.18.0: + version "0.18.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f" + integrity sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw== + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + +type-fest@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.4.1.tgz#8bdf77743385d8a4f13ba95f610f5ccd68c728f8" + integrity sha512-IwzA/LSfD2vC1/YDYMv/zHP4rDF1usCwllsDpbolT3D4fUepIO7f9K70jjmUewU/LmGUKJcwcVtDCpnKk4BPMw== + +type-fest@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" + integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== + +type-fest@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" + integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== + +type-fest@^1.0.1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" + integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== + +type-fest@^2.0.0: + version "2.12.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.12.1.tgz#d2be8f50bf5f8f0a5fd916d29bf3e98c17e960be" + integrity sha512-AiknQSEqKVGDDjtZqeKrUoTlcj7FKhupmnVUgz6KoOKtvMwRGE6hUNJ/nVear+h7fnUPO1q/htSkYKb1pyntkQ== + +typedarray-to-buffer@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" + integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== + dependencies: + is-typedarray "^1.0.0" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= + +typedoc@0.22.11: + version "0.22.11" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.22.11.tgz#a3d7f4577eef9fc82dd2e8f4e2915e69f884c250" + integrity sha512-pVr3hh6dkS3lPPaZz1fNpvcrqLdtEvXmXayN55czlamSgvEjh+57GUqfhAI1Xsuu/hNHUT1KNSx8LH2wBP/7SA== + dependencies: + glob "^7.2.0" + lunr "^2.3.9" + marked "^4.0.10" + minimatch "^3.0.4" + shiki "^0.10.0" + +typescript@4.5.2: + version "4.5.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.2.tgz#8ac1fba9f52256fdb06fb89e4122fa6a346c2998" + integrity sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw== + +typescript@~3.9.10: + version "3.9.10" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.10.tgz#70f3910ac7a51ed6bef79da7800690b19bf778b8" + integrity sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q== + +uglify-js@^3.1.4: + version "3.15.3" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.15.3.tgz#9aa82ca22419ba4c0137642ba0df800cb06e0471" + integrity sha512-6iCVm2omGJbsu3JWac+p6kUiOpg3wFO2f8lIXjfEb8RrmLjzog1wTPMmwKB7swfzzqxj9YM+sGUM++u1qN4qJg== + +uid-number@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" + integrity sha1-DqEOgDXo61uOREnwbaHHMGY7qoE= + +umask@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/umask/-/umask-1.1.0.tgz#f29cebf01df517912bb58ff9c4e50fde8e33320d" + integrity sha1-8pzr8B31F5ErtY/5xOUP3o4zMg0= + +unbox-primitive@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471" + integrity sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw== + dependencies: + function-bind "^1.1.1" + has-bigints "^1.0.1" + has-symbols "^1.0.2" + which-boxed-primitive "^1.0.2" + +unique-filename@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" + integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== + dependencies: + unique-slug "^2.0.0" + +unique-slug@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c" + integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w== + dependencies: + imurmurhash "^0.1.4" + +unique-string@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-3.0.0.tgz#84a1c377aff5fd7a8bc6b55d8244b2bd90d75b9a" + integrity sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ== + dependencies: + crypto-random-string "^4.0.0" + +universal-user-agent@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee" + integrity sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w== + +universalify@^0.1.0, universalify@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + +unpipe@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + +upath@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/upath/-/upath-2.0.1.tgz#50c73dea68d6f6b990f51d279ce6081665d61a8b" + integrity sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w== + +upper-case-first@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/upper-case-first/-/upper-case-first-2.0.2.tgz#992c3273f882abd19d1e02894cc147117f844324" + integrity sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg== + dependencies: + tslib "^2.0.3" + +upper-case@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-2.0.2.tgz#d89810823faab1df1549b7d97a76f8662bae6f7a" + integrity sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg== + dependencies: + tslib "^2.0.3" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +url@0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64" + integrity sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ= + dependencies: + punycode "1.3.2" + querystring "0.2.0" + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +util-promisify@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/util-promisify/-/util-promisify-2.1.0.tgz#3c2236476c4d32c5ff3c47002add7c13b9a82a53" + integrity sha1-PCI2R2xNMsX/PEcAKt18E7moKlM= + dependencies: + object.getownpropertydescriptors "^2.0.3" + +uuid@3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" + integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== + +uuid@^3.3.2: + version "3.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +v8-compile-cache-lib@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz#0582bcb1c74f3a2ee46487ceecf372e46bce53e8" + integrity sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA== + +v8-compile-cache@^2.0.3: + version "2.3.0" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" + integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== + +v8-to-istanbul@^8.1.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz#77b752fd3975e31bbcef938f85e9bd1c7a8d60ed" + integrity sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.1" + convert-source-map "^1.6.0" + source-map "^0.7.3" + +validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== + dependencies: + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + +validate-npm-package-name@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz#5fa912d81eb7d0c74afc140de7317f0ca7df437e" + integrity sha1-X6kS2B630MdK/BQN5zF/DKffQ34= + dependencies: + builtins "^1.0.3" + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +vm2@^3.9.8: + version "3.9.9" + resolved "https://registry.yarnpkg.com/vm2/-/vm2-3.9.9.tgz#c0507bc5fbb99388fad837d228badaaeb499ddc5" + integrity sha512-xwTm7NLh/uOjARRBs8/95H0e8fT3Ukw5D/JJWhxMbhKzNh1Nu981jQKvkep9iKYNxzlVrdzD0mlBGkDKZWprlw== + dependencies: + acorn "^8.7.0" + acorn-walk "^8.2.0" + +vscode-oniguruma@^1.6.1: + version "1.6.2" + resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.6.2.tgz#aeb9771a2f1dbfc9083c8a7fdd9cccaa3f386607" + integrity sha512-KH8+KKov5eS/9WhofZR8M8dMHWN2gTxjMsG4jd04YhpbPR91fUj7rYQ2/XjeHCJWbg7X++ApRIU9NUwM2vTvLA== + +vscode-textmate@5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-5.2.0.tgz#01f01760a391e8222fe4f33fbccbd1ad71aed74e" + integrity sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ== + +w3c-hr-time@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd" + integrity sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ== + dependencies: + browser-process-hrtime "^1.0.0" + +w3c-xmlserializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz#3e7104a05b75146cc60f564380b7f683acf1020a" + integrity sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA== + dependencies: + xml-name-validator "^3.0.0" + +walker@^1.0.7: + version "1.0.8" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" + integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== + dependencies: + makeerror "1.0.12" + +watchpack@^2.0.0-beta.10: + version "2.3.1" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.3.1.tgz#4200d9447b401156eeca7767ee610f8809bc9d25" + integrity sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + +wcwidth@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" + integrity sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g= + dependencies: + defaults "^1.0.3" + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= + +webidl-conversions@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff" + integrity sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA== + +webidl-conversions@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514" + integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== + +whatwg-encoding@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0" + integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw== + dependencies: + iconv-lite "0.4.24" + +whatwg-mimetype@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" + integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0= + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +whatwg-url@^8.0.0, whatwg-url@^8.4.0, whatwg-url@^8.5.0: + version "8.7.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.7.0.tgz#656a78e510ff8f3937bc0bcbe9f5c0ac35941b77" + integrity sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg== + dependencies: + lodash "^4.7.0" + tr46 "^2.1.0" + webidl-conversions "^6.1.0" + +which-boxed-primitive@^1.0.1, which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + dependencies: + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" + +which-collection@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906" + integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A== + dependencies: + is-map "^2.0.1" + is-set "^2.0.1" + is-weakmap "^2.0.1" + is-weakset "^2.0.1" + +which-typed-array@^1.1.2: + version "1.1.7" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.7.tgz#2761799b9a22d4b8660b3c1b40abaa7739691793" + integrity sha512-vjxaB4nfDqwKI0ws7wZpxIlde1XrLX5uB0ZjpfshgmapJMD7jJWhZI+yToJTqaFByF0eNBcYxbjmCzoRP7CfEw== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + es-abstract "^1.18.5" + foreach "^2.0.5" + has-tostringtag "^1.0.0" + is-typed-array "^1.1.7" + +which@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +which@^2.0.1, which@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wide-align@^1.1.0: + version "1.1.5" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" + integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== + dependencies: + string-width "^1.0.2 || 2 || 3 || 4" + +winston-transport@^4.4.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.5.0.tgz#6e7b0dd04d393171ed5e4e4905db265f7ab384fa" + integrity sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q== + dependencies: + logform "^2.3.2" + readable-stream "^3.6.0" + triple-beam "^1.3.0" + +winston@3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/winston/-/winston-3.3.3.tgz#ae6172042cafb29786afa3d09c8ff833ab7c9170" + integrity sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw== + dependencies: + "@dabh/diagnostics" "^2.0.2" + async "^3.1.0" + is-stream "^2.0.0" + logform "^2.2.0" + one-time "^1.0.0" + readable-stream "^3.4.0" + stack-trace "0.0.x" + triple-beam "^1.3.0" + winston-transport "^4.4.0" + +word-wrap@^1.2.3, word-wrap@~1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + +wordwrap@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= + +workerpool@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.0.tgz#827d93c9ba23ee2019c3ffaff5c27fccea289e8b" + integrity sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A== + +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +write-file-atomic@^2.4.2: + version "2.4.3" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.3.tgz#1fd2e9ae1df3e75b8d8c367443c692d4ca81f481" + integrity sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ== + dependencies: + graceful-fs "^4.1.11" + imurmurhash "^0.1.4" + signal-exit "^3.0.2" + +write-file-atomic@^3.0.0, write-file-atomic@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" + integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== + dependencies: + imurmurhash "^0.1.4" + is-typedarray "^1.0.0" + signal-exit "^3.0.2" + typedarray-to-buffer "^3.1.5" + +write-json-file@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/write-json-file/-/write-json-file-3.2.0.tgz#65bbdc9ecd8a1458e15952770ccbadfcff5fe62a" + integrity sha512-3xZqT7Byc2uORAatYiP3DHUUAVEkNOswEWNs9H5KXiicRTvzYzYqKjYc4G7p+8pltvAw641lVByKVtMpf+4sYQ== + dependencies: + detect-indent "^5.0.0" + graceful-fs "^4.1.15" + make-dir "^2.1.0" + pify "^4.0.1" + sort-keys "^2.0.0" + write-file-atomic "^2.4.2" + +write-json-file@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/write-json-file/-/write-json-file-4.3.0.tgz#908493d6fd23225344af324016e4ca8f702dd12d" + integrity sha512-PxiShnxf0IlnQuMYOPPhPkhExoCQuTUNPOa/2JWCYTmBquU9njyyDuwRKN26IZBlp4yn1nt+Agh2HOOBl+55HQ== + dependencies: + detect-indent "^6.0.0" + graceful-fs "^4.1.15" + is-plain-obj "^2.0.0" + make-dir "^3.0.0" + sort-keys "^4.0.0" + write-file-atomic "^3.0.0" + +write-pkg@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/write-pkg/-/write-pkg-4.0.0.tgz#675cc04ef6c11faacbbc7771b24c0abbf2a20039" + integrity sha512-v2UQ+50TNf2rNHJ8NyWttfm/EJUBWMJcx6ZTYZr6Qp52uuegWw/lBkCtCbnYZEmPRNL61m+u67dAmGxo+HTULA== + dependencies: + sort-keys "^2.0.0" + type-fest "^0.4.1" + write-json-file "^3.2.0" + +ws@^7.4.6: + version "7.5.7" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.7.tgz#9e0ac77ee50af70d58326ecff7e85eb3fa375e67" + integrity sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A== + +xml-name-validator@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" + integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== + +xml2js@0.4.19: + version "0.4.19" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" + integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q== + dependencies: + sax ">=0.6.0" + xmlbuilder "~9.0.1" + +xml@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5" + integrity sha1-eLpyAgApxbyHuKgaPPzXS0ovweU= + +xmlbuilder@^15.1.1: + version "15.1.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz#9dcdce49eea66d8d10b42cae94a79c3c8d0c2ec5" + integrity sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg== + +xmlbuilder@~9.0.1: + version "9.0.7" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" + integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0= + +xmlchars@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" + integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== + +xregexp@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943" + integrity sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM= + +xtend@~4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^3.0.0, yallist@^3.0.2, yallist@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yaml@1.10.2, yaml@^1.10.0, yaml@^1.10.2: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + +yargs-parser@20.2.4: + version "20.2.4" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" + integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== + +yargs-parser@20.x, yargs-parser@^20.2.2, yargs-parser@^20.2.3: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs-parser@^21.0.0: + version "21.0.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.0.1.tgz#0267f286c877a4f0f728fceb6f8a3e4cb95c6e35" + integrity sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg== + +yargs@17.3.0: + version "17.3.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.3.0.tgz#295c4ffd0eef148ef3e48f7a2e0f58d0e4f26b1c" + integrity sha512-GQl1pWyDoGptFPJx9b9L6kmR33TGusZvXIZUT+BOz9f7X2L94oeAskFYLEg/FkhV06zZPBYLvLZRWeYId29lew== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.0.0" + +yargs@^16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + +zip-stream@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-4.1.0.tgz#51dd326571544e36aa3f756430b313576dc8fc79" + integrity sha512-zshzwQW7gG7hjpBlgeQP9RuyPGNxvJdzR8SUM3QhxCnLjWN2E7j3dOvpeDcQoETfHx0urRS7EtmVToql7YpU4A== + dependencies: + archiver-utils "^2.1.0" + compress-commons "^4.1.0" + readable-stream "^3.6.0"