├── tests ├── acceptance │ ├── testdata │ │ └── s3-test-file.txt │ ├── utils_test.go │ ├── outputs_test.go │ └── artifacts_bucket_test.go ├── integration │ └── test.cfg.example └── testutils │ ├── mocks.go │ └── retry_test.go ├── docs ├── img │ ├── cognito.png │ ├── credspage.png │ ├── groupname.png │ ├── addtogroup.png │ ├── creategroup.png │ ├── createuser.png │ ├── quickstartuser.png │ ├── usersandgroups.png │ ├── manageuserpools.png │ ├── quickstartadmin.png │ ├── quickstartuserlogin.png │ ├── quickstartadmindetail.png │ └── quickstartadminlogin.png ├── swagger_link.rst ├── custom.css ├── requirements.txt ├── Makefile ├── make.bat ├── index.rst └── home.md ├── dist ├── favicon-16x16.png ├── favicon-32x32.png └── swagger-ui.css.map ├── modules ├── authentication │ ├── data.tf │ ├── fixtures │ │ └── iam │ │ │ ├── admin-policy.json │ │ │ ├── assume-role.json │ │ │ └── user-policy.json │ ├── variables.tf │ ├── outputs.tf │ └── iam.tf ├── ssm_parameter_names │ ├── variables.tf │ └── outputs.tf ├── cloudwatch_event │ ├── variables.tf │ └── main.tf ├── main.tf ├── lambda │ ├── outputs.tf │ ├── variables.tf │ └── lambda.tf ├── usage_lambda.tf ├── email_identity.tf ├── lease_auth_lambda.tf ├── credentials_web_page_lambda.tf ├── alarms_sns.tf ├── cloudwatch_dashboard.tf ├── artifacts_bucket.tf ├── leases_lambda.tf └── accounts_lambda.tf ├── .golangci.yml ├── pkg ├── event │ ├── helpers.go │ ├── mocks │ │ └── Publisher.go │ ├── eventiface │ │ └── servicer.go │ ├── sqs.go │ ├── eventbus.go │ ├── sns.go │ └── sqs_test.go ├── reset │ ├── testdata │ │ ├── test-config-template.yml │ │ └── test-config-result.yml │ ├── nuker.go │ └── athena_test.go ├── rolemanager │ ├── helpers.go │ └── mocks │ │ └── PolicyManager.go ├── errors │ ├── multierror_test.go │ ├── stack.go │ ├── multierror.go │ └── wrap.go ├── common │ ├── notification_test.go │ ├── mocks │ │ ├── TokenServiceExt.go │ │ ├── Notificationer.go │ │ └── Storager.go │ ├── token.go │ ├── notification.go │ └── queue.go ├── api │ ├── response │ │ ├── leaseauth.go │ │ ├── usage.go │ │ ├── account.go │ │ └── lease.go │ ├── helpers.go │ ├── mocks │ │ ├── UserDetailer.go │ │ └── Controller.go │ └── error.go ├── accountmanager │ ├── validate.go │ ├── accountmanageriface │ │ ├── servicer.go │ │ └── mocks │ │ │ └── Servicer.go │ ├── helpers.go │ ├── mocks │ │ └── clienter.go │ └── client.go ├── usage │ ├── mocks │ │ ├── Writer.go │ │ ├── MultipleReader.go │ │ ├── SingleReader.go │ │ ├── Reader.go │ │ ├── ReaderWriter.go │ │ └── DBer.go │ ├── validate.go │ └── validate_test.go ├── account │ ├── mocks │ │ ├── Deleter.go │ │ ├── Writer.go │ │ ├── SingleReader.go │ │ ├── MultipleReader.go │ │ ├── WriterDeleter.go │ │ ├── Manager.go │ │ ├── Reader.go │ │ └── Eventer.go │ ├── accountiface │ │ └── servicer.go │ ├── validate_test.go │ └── validate.go ├── lease │ ├── mocks │ │ ├── Writer.go │ │ ├── MultipleReader.go │ │ ├── AccountServicer.go │ │ ├── Eventer.go │ │ ├── SingleReader.go │ │ └── Reader.go │ ├── leaseiface │ │ └── servicer.go │ └── validate_test.go ├── data │ ├── dataiface │ │ ├── account.go │ │ └── lease.go │ ├── helpers_test.go │ └── helpers.go ├── config │ ├── mocks │ │ └── ConfigurationServiceBuilder.go │ └── configiface │ │ └── servicebuilder.go ├── db │ └── error.go ├── awsiface │ └── mocks │ │ └── AwsSession.go ├── email │ └── mocks │ │ └── Service.go └── budget │ ├── mocks │ └── Service.go │ ├── budget_test.go │ └── budget.go ├── cmd ├── lambda │ ├── credentials_web_page │ │ ├── public │ │ │ └── main.css │ │ ├── get.go │ │ └── views │ │ │ └── index.html │ ├── accounts │ │ ├── validate.go │ │ ├── get.go │ │ ├── main_test.go │ │ ├── delete.go │ │ ├── create.go │ │ ├── list.go │ │ └── update.go │ ├── leases │ │ ├── get.go │ │ └── list.go │ ├── lease_auth │ │ └── main.go │ └── update_principal_policy │ │ └── main.go └── codebuild │ └── reset │ ├── buildspec.yml │ └── default-nuke-config-template.yml ├── scripts ├── test_functional.sh ├── deploy_local │ ├── destroy_local_build.sh │ └── deploy_local_build.sh ├── test.sh ├── lint.sh └── deploy.sh ├── NOTICE.txt ├── .readthedocs.yml ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── pull_request_template.md ├── tools └── awsnukedocgen │ ├── awsnukedocgen.yaml │ └── samples │ └── nuke │ └── resources │ ├── iam-users.go.txt │ ├── sns-topics.go.txt │ ├── sqs-queues.go.txt │ ├── glue-jobs.go.txt │ ├── mq-broker.go.txt │ ├── iot-jobs.go.txt │ ├── batch-jobqueues.go.txt │ ├── autoscaling-groups.go.txt │ ├── waf-rules.go.txt │ ├── cloud9-environments.go.txt │ ├── neptune-instances.go.txt │ ├── emr-securityconfigurations.go.txt │ ├── opsworks-instances.go.txt │ ├── autoscaling-launchconfigurations.go.txt │ ├── kinesis-streams.go.txt │ ├── msk-cluster.go.txt │ ├── sns-subscriptions.go.txt │ ├── route53-hosted-zones.go.txt │ ├── redshift-clusters.go.txt │ ├── machinelearning-batchpredictions.go.txt │ ├── emr-clusters.go.txt │ ├── route53-health-checks.go.txt │ ├── firehose-deliverystreams.go.txt │ ├── lambda-functions.go.txt │ ├── iam-virtual-mfa-devices.go.txt │ ├── sns-platformapplications.go.txt │ ├── elb-elb.go.txt │ └── s3-multipart-uploads.go.txt ├── index.html ├── .gitignore ├── Makefile └── README.md /tests/acceptance/testdata/s3-test-file.txt: -------------------------------------------------------------------------------- 1 | test -------------------------------------------------------------------------------- /docs/img/cognito.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nelsonjchen/dce/master/docs/img/cognito.png -------------------------------------------------------------------------------- /docs/swagger_link.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ============= 3 | 4 | .. swaggerv2doc:: swagger.json -------------------------------------------------------------------------------- /dist/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nelsonjchen/dce/master/dist/favicon-16x16.png -------------------------------------------------------------------------------- /dist/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nelsonjchen/dce/master/dist/favicon-32x32.png -------------------------------------------------------------------------------- /docs/img/credspage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nelsonjchen/dce/master/docs/img/credspage.png -------------------------------------------------------------------------------- /docs/img/groupname.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nelsonjchen/dce/master/docs/img/groupname.png -------------------------------------------------------------------------------- /docs/img/addtogroup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nelsonjchen/dce/master/docs/img/addtogroup.png -------------------------------------------------------------------------------- /docs/img/creategroup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nelsonjchen/dce/master/docs/img/creategroup.png -------------------------------------------------------------------------------- /docs/img/createuser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nelsonjchen/dce/master/docs/img/createuser.png -------------------------------------------------------------------------------- /docs/img/quickstartuser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nelsonjchen/dce/master/docs/img/quickstartuser.png -------------------------------------------------------------------------------- /docs/img/usersandgroups.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nelsonjchen/dce/master/docs/img/usersandgroups.png -------------------------------------------------------------------------------- /docs/img/manageuserpools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nelsonjchen/dce/master/docs/img/manageuserpools.png -------------------------------------------------------------------------------- /docs/img/quickstartadmin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nelsonjchen/dce/master/docs/img/quickstartadmin.png -------------------------------------------------------------------------------- /dist/swagger-ui.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":[],"names":[],"mappings":"","file":"swagger-ui.css","sourceRoot":""} -------------------------------------------------------------------------------- /docs/img/quickstartuserlogin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nelsonjchen/dce/master/docs/img/quickstartuserlogin.png -------------------------------------------------------------------------------- /modules/authentication/data.tf: -------------------------------------------------------------------------------- 1 | data "aws_caller_identity" "current" {} 2 | 3 | data "aws_region" "current" {} 4 | -------------------------------------------------------------------------------- /docs/img/quickstartadmindetail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nelsonjchen/dce/master/docs/img/quickstartadmindetail.png -------------------------------------------------------------------------------- /docs/img/quickstartadminlogin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nelsonjchen/dce/master/docs/img/quickstartadminlogin.png -------------------------------------------------------------------------------- /modules/ssm_parameter_names/variables.tf: -------------------------------------------------------------------------------- 1 | variable "namespace" { 2 | description = "The namespace for this Terraform run" 3 | } -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 5m0s 3 | 4 | issues: 5 | exclude: 6 | - "composite literal uses unkeyed fields" 7 | - "`main` is unused" -------------------------------------------------------------------------------- /pkg/event/helpers.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | type updateEvent struct { 4 | Old interface{} `json:"old"` 5 | New interface{} `json:"new"` 6 | } 7 | -------------------------------------------------------------------------------- /docs/custom.css: -------------------------------------------------------------------------------- 1 | /* 2 | https://github.com/mkdocs/mkdocs/issues/318 3 | Hide duplicate h1 title in nav 4 | */ 5 | li.toctree-l2:first-child { 6 | display: none; 7 | } -------------------------------------------------------------------------------- /pkg/reset/testdata/test-config-template.yml: -------------------------------------------------------------------------------- 1 | accounts: 2 | "{{id}}": 3 | filters: 4 | {{service}}: 5 | - "Account-IAM" 6 | - "IAM-Password-Policy" -------------------------------------------------------------------------------- /pkg/reset/testdata/test-config-result.yml: -------------------------------------------------------------------------------- 1 | accounts: 2 | "123456789012": 3 | filters: 4 | CloudFormationStack: 5 | - "Account-IAM" 6 | - "IAM-Password-Policy" -------------------------------------------------------------------------------- /cmd/lambda/credentials_web_page/public/main.css: -------------------------------------------------------------------------------- 1 | .center-block { 2 | display: block; 3 | max-width: 300px; 4 | margin: auto; 5 | margin-bottom: 1%; 6 | } 7 | 8 | .center-text { 9 | text-align: center; 10 | } -------------------------------------------------------------------------------- /scripts/test_functional.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euxo pipefail 3 | 4 | mkdir -p junit-report 5 | 6 | # Run functional tests 7 | go test -v ./tests/... -test.timeout 50m 2>&1 | tee >(go-junit-report > junit-report/functional.xml) 8 | 9 | -------------------------------------------------------------------------------- /modules/authentication/fixtures/iam/admin-policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": "execute-api:Invoke", 7 | "Resource": "${api_gateway_arn}/*" 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /cmd/lambda/accounts/validate.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | ) 7 | 8 | func isNil(value interface{}) error { 9 | if !reflect.ValueOf(value).IsNil() { 10 | return errors.New("must be empty") 11 | } 12 | return nil 13 | } 14 | -------------------------------------------------------------------------------- /modules/cloudwatch_event/variables.tf: -------------------------------------------------------------------------------- 1 | variable "lambda_function_arn" { 2 | type = string 3 | } 4 | variable schedule_expression { 5 | type = string 6 | } 7 | variable name { 8 | type = string 9 | } 10 | variable description { 11 | type = string 12 | } 13 | 14 | variable "enabled" { 15 | type = bool 16 | default = true 17 | } 18 | -------------------------------------------------------------------------------- /modules/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = "~>0.12.0" 3 | } 4 | 5 | provider "aws" { 6 | region = var.aws_region 7 | version = "2.65.0" 8 | } 9 | 10 | # Current AWS Account User 11 | data "aws_caller_identity" "current" { 12 | } 13 | 14 | locals { 15 | account_id = data.aws_caller_identity.current.account_id 16 | } 17 | 18 | -------------------------------------------------------------------------------- /tests/acceptance/utils_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/Optum/dce/tests/testutils" 7 | ) 8 | 9 | func givenEmptySystem(t *testing.T) { 10 | truncateDBTables(t, dbSvc) 11 | truncateUsageTable(t, usageSvc) 12 | testutils.GivenSqsIsEmpty(t, sqsSvc, sqsResetURL) 13 | testutils.GivenCodeBuildIsEmpty(t, codeBuildSvc, codeBuildResetName) 14 | } 15 | -------------------------------------------------------------------------------- /pkg/rolemanager/helpers.go: -------------------------------------------------------------------------------- 1 | package rolemanager 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws/awserr" 5 | "github.com/aws/aws-sdk-go/service/iam" 6 | ) 7 | 8 | func isAWSAlreadyExistsError(err error) bool { 9 | aerr, ok := err.(awserr.Error) 10 | if ok { 11 | switch aerr.Code() { 12 | case iam.ErrCodeEntityAlreadyExistsException: 13 | return true 14 | } 15 | } 16 | 17 | return false 18 | } 19 | -------------------------------------------------------------------------------- /cmd/codebuild/reset/buildspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.1 2 | 3 | phases: 4 | install: 5 | commands: 6 | - echo "Phase - install" 7 | - ls 8 | - pwd 9 | - echo "Install" 10 | - chmod +x reset 11 | - aws --version 12 | build: 13 | commands: 14 | - echo "Phase - Reset" 15 | - ./reset 16 | - aws --version 17 | post_build: 18 | commands: 19 | - echo "Phase - Post Reset" -------------------------------------------------------------------------------- /modules/lambda/outputs.tf: -------------------------------------------------------------------------------- 1 | output arn { 2 | value = aws_lambda_function.fn.arn 3 | } 4 | 5 | output name { 6 | value = aws_lambda_function.fn.function_name 7 | } 8 | 9 | output invoke_arn { 10 | value = aws_lambda_function.fn.invoke_arn 11 | } 12 | 13 | output execution_role_name { 14 | value = aws_iam_role.lambda_execution.name 15 | } 16 | output execution_role_arn { 17 | value = aws_iam_role.lambda_execution.arn 18 | } 19 | -------------------------------------------------------------------------------- /pkg/errors/multierror_test.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestMultiError(t *testing.T) { 11 | 12 | t.Run("new multierror", func(t *testing.T) { 13 | 14 | err1 := fmt.Errorf("err1") 15 | err2 := fmt.Errorf("err2") 16 | 17 | errs := NewMultiError("many errors", []error{err1, err2}) 18 | 19 | assert.Equal(t, errs.Error(), "many errors: err1; err2") 20 | }) 21 | 22 | } 23 | -------------------------------------------------------------------------------- /modules/ssm_parameter_names/outputs.tf: -------------------------------------------------------------------------------- 1 | output identity_pool_id { 2 | value = "/${var.namespace}/auth/identity_pool_id" 3 | } 4 | 5 | output user_pool_domain { 6 | value = "/${var.namespace}/auth/user_pool_domain" 7 | } 8 | 9 | output client_id { 10 | value = "/${var.namespace}/auth/client_id" 11 | } 12 | 13 | output user_pool_id { 14 | value = "/${var.namespace}/auth/user_pool_id" 15 | } 16 | 17 | output user_pool_endpoint { 18 | value = "/${var.namespace}/auth/user_pool_endpoint" 19 | } -------------------------------------------------------------------------------- /scripts/deploy_local/destroy_local_build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Destroy Local DCE deployment 4 | # 5 | # Example: 6 | # ./scripts/deploy_local/destroy_local_build.sh 7 | 8 | set -euxo pipefail 9 | 10 | KEY="local-tf-state" 11 | TABLE="local-tf-state" 12 | NAMESPACE=${DCE_NAMESPACE:-$(whoami)} 13 | 14 | cd modules 15 | terraform destroy -var="namespace=$NAMESPACE" 16 | rm -rf .terraform 17 | cd ../scripts/deploy_local 18 | terraform destroy -var="namespace=$NAMESPACE" 19 | rm -rf .terraform 20 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euxo pipefail 3 | 4 | mkdir -p junit-report 5 | 6 | # Run tests 7 | go test -v -coverprofile=coverage.txt -covermode count \ 8 | ./pkg/... ./cmd/... 2>&1 | \ 9 | tee test.output.txt | \ 10 | tee >(go-junit-report -set-exit-code > junit-report/report.xml) 11 | 12 | # Echo Test Output 13 | cat test.output.txt 14 | 15 | # Convert coverate to xml and html 16 | gocov convert coverage.txt > coverage.json 17 | gocov-xml < coverage.json > coverage.xml 18 | -------------------------------------------------------------------------------- /modules/authentication/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | type = string 3 | } 4 | 5 | variable "namespace" { 6 | type = string 7 | } 8 | 9 | variable "api_gateway_arn" { 10 | type = string 11 | } 12 | 13 | variable "callback_urls" { 14 | type = list(string) 15 | default = ["http://localhost:8080"] 16 | } 17 | 18 | variable "logout_urls" { 19 | type = list(string) 20 | default = ["http://localhost:8080"] 21 | } 22 | 23 | variable "identity_providers" { 24 | type = list(string) 25 | default = ["COGNITO"] 26 | } 27 | -------------------------------------------------------------------------------- /pkg/common/notification_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/stretchr/testify/require" 5 | "testing" 6 | ) 7 | 8 | func TestPrepareSNSMessageJSON(t *testing.T) { 9 | 10 | t.Run("should prepare an SNS message as JSON", func(t *testing.T) { 11 | obj := struct { 12 | Foo string `json:"foo"` 13 | }{ 14 | Foo: "bar", 15 | } 16 | message, err := PrepareSNSMessageJSON(obj) 17 | 18 | require.Nil(t, err) 19 | require.Equal(t, message, `{"default":"{\"foo\":\"bar\"}","Body":"{\"foo\":\"bar\"}"}`) 20 | }) 21 | 22 | } 23 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | DCE 2 | 3 | Copyright 2019 Optum 4 | 5 | Project Description: 6 | ==================== 7 | Disposable Cloud Environment (DCE) manages ephemeral AWS accounts for easy and secure access to the cloud. 8 | 9 | Author(s): 10 | Ranjan Prasad (@robologic) 11 | Jaya Nanda (@jayanandagit) 12 | Fabrizio Torelli (@hellgate75) 13 | Edan Schwartz (@eschwartz) 14 | Nathan A. Good (@nathanagood) 15 | Kevin DeJong (@kddejong) 16 | Matthew Meyers (@marinatedpork) 17 | Amudha Palani (@AmudhaPalani) 18 | Joshua Marsh (@joshmarsh) 19 | Kapil Thangavelu (@kapilt) 20 | -------------------------------------------------------------------------------- /cmd/lambda/accounts/get.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gorilla/mux" 7 | 8 | "github.com/Optum/dce/pkg/api" 9 | ) 10 | 11 | // GetAccountByID - Returns the single account by ID 12 | func GetAccountByID(w http.ResponseWriter, r *http.Request) { 13 | 14 | accountID := mux.Vars(r)["accountId"] 15 | 16 | account, err := Services.AccountService().Get(accountID) 17 | 18 | if err != nil { 19 | api.WriteAPIErrorResponse(w, err) 20 | return 21 | } 22 | 23 | api.WriteAPIResponse(w, http.StatusOK, account) 24 | } 25 | -------------------------------------------------------------------------------- /modules/authentication/fixtures/iam/assume-role.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": { 4 | "Effect": "Allow", 5 | "Principal": { 6 | "Federated": "cognito-identity.amazonaws.com" 7 | }, 8 | "Action": "sts:AssumeRoleWithWebIdentity", 9 | "Condition": { 10 | "StringEquals": { 11 | "cognito-identity.amazonaws.com:aud": "${cognito_identity_pool_id}" 12 | }, 13 | "ForAnyValue:StringLike": { 14 | "cognito-identity.amazonaws.com:amr": "authenticated" 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /modules/usage_lambda.tf: -------------------------------------------------------------------------------- 1 | module "usage_lambda" { 2 | source = "./lambda" 3 | name = "usage-${var.namespace}" 4 | namespace = var.namespace 5 | description = "API /usage endpoints" 6 | global_tags = var.global_tags 7 | handler = "usage" 8 | alarm_topic_arn = aws_sns_topic.alarms_topic.arn 9 | 10 | environment = { 11 | DEBUG = "false" 12 | NAMESPACE = var.namespace 13 | AWS_CURRENT_REGION = var.aws_region 14 | USAGE_CACHE_DB = aws_dynamodb_table.usage.id 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /modules/authentication/fixtures/iam/user-policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": "execute-api:Invoke", 7 | "Resource": [ 8 | "${api_gateway_arn}/GET/usage", 9 | "${api_gateway_arn}/GET/leases", 10 | "${api_gateway_arn}/GET/leases/*", 11 | "${api_gateway_arn}/POST/leases", 12 | "${api_gateway_arn}/POST/leases/*", 13 | "${api_gateway_arn}/DELETE/leases", 14 | "${api_gateway_arn}/DELETE/leases/*" 15 | 16 | ] 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Build documentation in the docs/ directory with Sphinx 9 | sphinx: 10 | configuration: docs/conf.py 11 | 12 | # Optionally build your docs in additional formats such as PDF and ePub 13 | formats: all 14 | 15 | # Optionally set the version of Python and requirements required to build your docs 16 | python: 17 | version: 3.7 18 | install: 19 | - requirements: docs/requirements.txt -------------------------------------------------------------------------------- /cmd/lambda/accounts/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestMain(m *testing.M) { 9 | os.Setenv("ACCOUNT_CREATED_TOPIC_ARN", "mock-account-created-topic") 10 | os.Setenv("PRINCIPAL_ROLE_NAME", "DCEPrincipal") 11 | os.Setenv("RESET_SQS_URL", "mock.queue.url") 12 | os.Setenv("PRINCIPAL_MAX_SESSION_DURATION", "100") 13 | os.Setenv("PRINCIPAL_POLICY_NAME", "DCEPrincipalDefaultPolicy") 14 | os.Setenv("PRINCIPAL_IAM_DENY_TAGS", "DCE,CantTouchThis") 15 | os.Setenv("ACCOUNT_DELETED_TOPIC_ARN", "test:arn") 16 | os.Exit(m.Run()) 17 | } 18 | -------------------------------------------------------------------------------- /pkg/event/mocks/Publisher.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // Publisher is an autogenerated mock type for the Publisher type 8 | type Publisher struct { 9 | mock.Mock 10 | } 11 | 12 | // Publish provides a mock function with given fields: i 13 | func (_m *Publisher) Publish(i interface{}) error { 14 | ret := _m.Called(i) 15 | 16 | var r0 error 17 | if rf, ok := ret.Get(0).(func(interface{}) error); ok { 18 | r0 = rf(i) 19 | } else { 20 | r0 = ret.Error(0) 21 | } 22 | 23 | return r0 24 | } 25 | -------------------------------------------------------------------------------- /pkg/api/response/leaseauth.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | // LeaseAuthResponse is the structured JSON Response for an Lease 4 | // to be returned for APIs 5 | // { 6 | // "accessKeyId": "AKIAI44QH8DHBEXAMPLE", 7 | // "secretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", 8 | // "sessionKey": "AQoDYXdzEJr...", 9 | // "consoleUrl": "https://aws.amazon.com/console/", 10 | // } 11 | type LeaseAuthResponse struct { 12 | AccessKeyID string `json:"accessKeyId"` 13 | SecretAccessKey string `json:"secretAccessKey"` 14 | SessionToken string `json:"sessionToken"` 15 | ConsoleURL string `json:"consoleUrl"` 16 | } 17 | -------------------------------------------------------------------------------- /pkg/accountmanager/validate.go: -------------------------------------------------------------------------------- 1 | package accountmanager 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/Optum/dce/pkg/arn" 7 | validation "github.com/go-ozzo/ozzo-validation" 8 | ) 9 | 10 | func isAssumable(client clienter) validation.RuleFunc { 11 | return func(value interface{}) error { 12 | a, ok := value.(*arn.ARN) 13 | if !ok { 14 | return fmt.Errorf("value is not an ARN") 15 | } 16 | // Create the credentials from AssumeRoleProvider to assume the role 17 | config := client.Config(a) 18 | _, err := config.Credentials.Get() 19 | if err != nil { 20 | return err 21 | } 22 | 23 | return nil 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /pkg/usage/mocks/Writer.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | import usage "github.com/Optum/dce/pkg/usage" 7 | 8 | // Writer is an autogenerated mock type for the Writer type 9 | type Writer struct { 10 | mock.Mock 11 | } 12 | 13 | // Write provides a mock function with given fields: i 14 | func (_m *Writer) Write(i *usage.Usage) error { 15 | ret := _m.Called(i) 16 | 17 | var r0 error 18 | if rf, ok := ret.Get(0).(func(*usage.Usage) error); ok { 19 | r0 = rf(i) 20 | } else { 21 | r0 = ret.Error(0) 22 | } 23 | 24 | return r0 25 | } 26 | -------------------------------------------------------------------------------- /pkg/account/mocks/Deleter.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import account "github.com/Optum/dce/pkg/account" 6 | import mock "github.com/stretchr/testify/mock" 7 | 8 | // Deleter is an autogenerated mock type for the Deleter type 9 | type Deleter struct { 10 | mock.Mock 11 | } 12 | 13 | // Delete provides a mock function with given fields: i 14 | func (_m *Deleter) Delete(i *account.Account) error { 15 | ret := _m.Called(i) 16 | 17 | var r0 error 18 | if rf, ok := ret.Get(0).(func(*account.Account) error); ok { 19 | r0 = rf(i) 20 | } else { 21 | r0 = ret.Error(0) 22 | } 23 | 24 | return r0 25 | } 26 | -------------------------------------------------------------------------------- /pkg/accountmanager/accountmanageriface/servicer.go: -------------------------------------------------------------------------------- 1 | // 2 | 3 | package accountmanageriface 4 | 5 | import ( 6 | "github.com/Optum/dce/pkg/account" 7 | "github.com/Optum/dce/pkg/arn" 8 | ) 9 | 10 | // Servicer makes working with the Account Manager easier 11 | type Servicer interface { 12 | // ValidateAccess creates a new Account instance 13 | ValidateAccess(role *arn.ARN) error 14 | // UpsertPrincipalAccess creates roles, policies and update them as needed 15 | UpsertPrincipalAccess(account *account.Account) error 16 | // DeletePrincipalAccess removes all the principal roles and policies 17 | DeletePrincipalAccess(account *account.Account) error 18 | } 19 | -------------------------------------------------------------------------------- /pkg/accountmanager/helpers.go: -------------------------------------------------------------------------------- 1 | package accountmanager 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws/awserr" 5 | "github.com/aws/aws-sdk-go/service/iam" 6 | ) 7 | 8 | func isAWSAlreadyExistsError(err error) bool { 9 | aerr, ok := err.(awserr.Error) 10 | if ok { 11 | switch aerr.Code() { 12 | case iam.ErrCodeEntityAlreadyExistsException: 13 | return true 14 | } 15 | } 16 | 17 | return false 18 | } 19 | 20 | func isAWSNoSuchEntityError(err error) bool { 21 | aerr, ok := err.(awserr.Error) 22 | if ok { 23 | switch aerr.Code() { 24 | case iam.ErrCodeNoSuchEntityException: 25 | return true 26 | } 27 | } 28 | 29 | return false 30 | } 31 | -------------------------------------------------------------------------------- /cmd/lambda/accounts/delete.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/Optum/dce/pkg/api" 7 | "github.com/gorilla/mux" 8 | ) 9 | 10 | // DeleteAccount - Deletes the account 11 | func DeleteAccount(w http.ResponseWriter, r *http.Request) { 12 | 13 | accountID := mux.Vars(r)["accountId"] 14 | 15 | acct, err := Services.AccountService().Get(accountID) 16 | if err != nil { 17 | api.WriteAPIErrorResponse(w, err) 18 | return 19 | } 20 | 21 | err = Services.AccountService().Delete(acct) 22 | if err != nil { 23 | api.WriteAPIErrorResponse(w, err) 24 | return 25 | } 26 | 27 | api.WriteAPIResponse(w, http.StatusNoContent, nil) 28 | } 29 | -------------------------------------------------------------------------------- /pkg/common/mocks/TokenServiceExt.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | awsMocks "github.com/Optum/dce/pkg/awsiface/mocks" 5 | "github.com/aws/aws-sdk-go/aws" 6 | "github.com/aws/aws-sdk-go/aws/client" 7 | "github.com/stretchr/testify/mock" 8 | ) 9 | 10 | func (_m *TokenService) MockNewSession(expectedRoleArn string) *awsMocks.AwsSession { 11 | mockAssumedSession := &awsMocks.AwsSession{} 12 | mockAssumedSession. 13 | On("ClientConfig", mock.Anything). 14 | Return(client.Config{ 15 | Config: &aws.Config{}, 16 | }) 17 | 18 | _m. 19 | On("NewSession", mock.Anything, expectedRoleArn). 20 | Return(mockAssumedSession, nil) 21 | 22 | return mockAssumedSession 23 | } 24 | -------------------------------------------------------------------------------- /pkg/api/response/usage.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | // UsageResponse is the serialized JSON Response for an account usage 4 | // to be returned by usage API 5 | type UsageResponse struct { 6 | PrincipalID string `json:"principalId"` // User Principal ID 7 | AccountID string `json:"accountId"` // AWS Account ID 8 | StartDate int64 `json:"startDate"` // Usage start date Epoch Timestamp 9 | EndDate int64 `json:"endDate"` // Usage ends date Epoch Timestamp 10 | CostAmount float64 `json:"costAmount"` // Cost Amount for given period 11 | CostCurrency string `json:"costCurrency"` // Cost currency 12 | TimeToLive int64 `json:"timeToLive"` // ttl attribute 13 | } 14 | -------------------------------------------------------------------------------- /tests/integration/test.cfg.example: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | #------------------------------------------------------------------------------ 4 | # test.cfg 5 | # This is an example file for test.cfg, which is read by the testmain.sh script 6 | # for certain environment configurations. To use this file, copy it to test.cfg 7 | # and uncomment the configuration values that are documented below. 8 | #------------------------------------------------------------------------------ 9 | 10 | # Uncomment if you want to specify the version of DCE CLI to test. This version 11 | # will be used to download the DCE CLI. If it is not present, the latest 12 | # version will be used. 13 | # 14 | # DCE_CLI_VERSION=v0.4.0 15 | 16 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | 2 | alabaster==0.7.12 3 | argh==0.26.2 4 | Babel==2.7.0 5 | certifi==2019.11.28 6 | chardet==3.0.4 7 | docutils==0.15.2 8 | future==0.18.2 9 | idna==2.8 10 | imagesize==1.1.0 11 | Jinja2==2.10.3 12 | livereload==2.6.1 13 | m2r==0.2.1 14 | MarkupSafe==1.1.1 15 | mistune==0.8.4 16 | packaging==19.2 17 | pathtools==0.1.2 18 | port-for==0.3.1 19 | Pygments==2.5.2 20 | pyparsing==2.4.5 21 | pytz==2019.3 22 | PyYAML==5.2 23 | requests==2.22.0 24 | requests-file==1.4.3 25 | six==1.13.0 26 | snowballstemmer==2.0.0 27 | Sphinx==1.8.5 28 | sphinx-autobuild==0.7.1 29 | sphinx-rtd-theme==0.4.3 30 | sphinxcontrib-swaggerdoc==0.1.7 31 | sphinxcontrib-websupport==1.1.2 32 | tornado==6.0.3 33 | urllib3==1.25.7 34 | watchdog==0.9.0 -------------------------------------------------------------------------------- /pkg/account/mocks/Writer.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import account "github.com/Optum/dce/pkg/account" 6 | import mock "github.com/stretchr/testify/mock" 7 | 8 | // Writer is an autogenerated mock type for the Writer type 9 | type Writer struct { 10 | mock.Mock 11 | } 12 | 13 | // Write provides a mock function with given fields: i, lastModifiedOn 14 | func (_m *Writer) Write(i *account.Account, lastModifiedOn *int64) error { 15 | ret := _m.Called(i, lastModifiedOn) 16 | 17 | var r0 error 18 | if rf, ok := ret.Get(0).(func(*account.Account, *int64) error); ok { 19 | r0 = rf(i, lastModifiedOn) 20 | } else { 21 | r0 = ret.Error(0) 22 | } 23 | 24 | return r0 25 | } 26 | -------------------------------------------------------------------------------- /pkg/lease/mocks/Writer.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import lease "github.com/Optum/dce/pkg/lease" 6 | import mock "github.com/stretchr/testify/mock" 7 | 8 | // Writer is an autogenerated mock type for the Writer type 9 | type Writer struct { 10 | mock.Mock 11 | } 12 | 13 | // Write provides a mock function with given fields: input, lastModifiedOn 14 | func (_m *Writer) Write(input *lease.Lease, lastModifiedOn *int64) error { 15 | ret := _m.Called(input, lastModifiedOn) 16 | 17 | var r0 error 18 | if rf, ok := ret.Get(0).(func(*lease.Lease, *int64) error); ok { 19 | r0 = rf(input, lastModifiedOn) 20 | } else { 21 | r0 = ret.Error(0) 22 | } 23 | 24 | return r0 25 | } 26 | -------------------------------------------------------------------------------- /pkg/api/helpers.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | 7 | "github.com/Optum/dce/pkg/errors" 8 | "github.com/gorilla/schema" 9 | ) 10 | 11 | // BuildNextURL merges the next parameters of pagination into the request parameters and returns an API URL. 12 | func BuildNextURL(u url.URL, i interface{}) (url.URL, error) { 13 | req := url.URL{ 14 | Scheme: u.Scheme, 15 | Host: u.Host, 16 | Path: u.Path, 17 | } 18 | 19 | values := url.Values{} 20 | err := schema.NewEncoder().Encode(i, values) 21 | if err != nil { 22 | fmt.Printf("%+v\n", err) 23 | return url.URL{}, errors.NewInternalServer("unable to encode query", err) 24 | } 25 | 26 | req.RawQuery = values.Encode() 27 | return req, nil 28 | } 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | 12 | 13 | **Describe the solution you'd like** 14 | 15 | 16 | **Describe alternatives you've considered** 17 | 18 | 19 | **Additional context** 20 | 21 | -------------------------------------------------------------------------------- /tools/awsnukedocgen/awsnukedocgen.yaml: -------------------------------------------------------------------------------- 1 | additionalServices: 2 | - neptune-db 3 | - sts 4 | - apigateway 5 | - execute-api 6 | - tag 7 | - waf-regional 8 | - application-autoscaling 9 | serviceAliases: 10 | acmpca: acm-pca 11 | cloudhsmv2: cloudhsm 12 | cloudwatchlogs: logs 13 | cloudwatchevents: events 14 | cognitoidentity: cognito-identity 15 | cognitoidentityprovider: cognito-idp 16 | configservice: config 17 | databasemigrationservice: dms 18 | directoryservice: ds 19 | efs: elasticfilesystem 20 | elasticsearchservice: es 21 | elb: elasticloadbalancing 22 | elbv2: elasticloadbalancing 23 | emr: elasticmapreduce 24 | mobile: mobilehub 25 | opsworkscm: opsworks-cm 26 | resourcegroups: resource-groups 27 | sfn: states 28 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = . 8 | BUILDDIR = _build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | livehtml: 17 | sphinx-autobuild -b html "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) 18 | 19 | # Catch-all target: route all unknown targets to Sphinx using the new 20 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 21 | %: Makefile 22 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /pkg/data/dataiface/account.go: -------------------------------------------------------------------------------- 1 | // 2 | 3 | package dataiface 4 | 5 | import ( 6 | "github.com/Optum/dce/pkg/account" 7 | ) 8 | 9 | // AccountData makes working with the Account Data Layer easier 10 | type AccountData interface { 11 | // Write the Account record in DynamoDB 12 | // This is an upsert operation in which the record will either 13 | // be inserted or updated 14 | // prevLastModifiedOn parameter is the original lastModifiedOn 15 | Write(account *account.Account, prevLastModifiedOn *int64) error 16 | // Delete the Account record in DynamoDB 17 | Delete(account *account.Account) error 18 | // Get the Account record by ID 19 | Get(ID string) (*account.Account, error) 20 | // List Get a list of accounts 21 | List(query *account.Account) (*account.Accounts, error) 22 | } 23 | -------------------------------------------------------------------------------- /cmd/lambda/leases/get.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gorilla/mux" 7 | 8 | "github.com/Optum/dce/pkg/api" 9 | ) 10 | 11 | // GetLeaseByID - Returns the single lease by ID 12 | func GetLeaseByID(w http.ResponseWriter, r *http.Request) { 13 | 14 | leaseID := mux.Vars(r)["leaseID"] 15 | 16 | lease, err := Services.LeaseService().Get(leaseID) 17 | 18 | if err != nil { 19 | api.WriteAPIErrorResponse(w, err) 20 | return 21 | } 22 | 23 | //If user is not an admin, they can't get leases for other users 24 | user := r.Context().Value(api.User{}).(*api.User) 25 | err = user.Authorize(*lease.PrincipalID) 26 | if err != nil { 27 | api.WriteAPIErrorResponse(w, err) 28 | return 29 | } 30 | 31 | api.WriteAPIResponse(w, http.StatusOK, lease) 32 | } 33 | -------------------------------------------------------------------------------- /scripts/deploy_local/deploy_local_build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Deploy DCE master account from a local machine 4 | # 5 | # Example: 6 | # ./scripts/deploy_local/deploy_local_build.sh 7 | 8 | set -euxo pipefail 9 | 10 | KEY="local-tf-state" 11 | REGION="us-east-1" 12 | TABLE="local-tf-state" 13 | NAMESPACE=${DCE_NAMESPACE:-$(whoami)} 14 | BUILD_ARTIFACT="bin/build_artifacts.zip" 15 | 16 | cd scripts/deploy_local && terraform init 17 | terraform apply -var="namespace=$NAMESPACE" 18 | BUCKET=$(terraform output bucket) 19 | cd ../../modules 20 | terraform init -backend-config="bucket=$BUCKET" -backend-config="key=$KEY" 21 | terraform apply -var="namespace=$NAMESPACE" 22 | ARTBUCKET=$(terraform output artifacts_bucket_name) 23 | cd ../ 24 | scripts/deploy.sh $BUILD_ARTIFACT $NAMESPACE $ARTBUCKET 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Version information** 11 | 17 | 18 | **Describe the bug** 19 | 20 | 21 | **To Reproduce** 22 | 29 | 30 | **Expected behavior** 31 | 32 | 33 | **Additional context** 34 | 35 | -------------------------------------------------------------------------------- /pkg/errors/stack.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "fmt" 5 | "github.com/pkg/errors" 6 | "runtime" 7 | ) 8 | 9 | // stack represents a stack of program counters. 10 | type stack []uintptr 11 | 12 | func (s *stack) Format(st fmt.State, verb rune) { 13 | switch verb { 14 | case 'v': 15 | switch { 16 | case st.Flag('+'): 17 | for _, pc := range *s { 18 | f := errors.Frame(pc) 19 | fmt.Fprintf(st, "\n%+v", f) 20 | } 21 | } 22 | } 23 | } 24 | 25 | func (s *stack) StackTrace() errors.StackTrace { 26 | f := make([]errors.Frame, len(*s)) 27 | for i := 0; i < len(f); i++ { 28 | f[i] = errors.Frame((*s)[i]) 29 | } 30 | return f 31 | } 32 | 33 | func callers() *stack { 34 | const depth = 32 35 | var pcs [depth]uintptr 36 | n := runtime.Callers(3, pcs[:]) 37 | var st stack = pcs[0:n] 38 | return &st 39 | } 40 | -------------------------------------------------------------------------------- /modules/lambda/variables.tf: -------------------------------------------------------------------------------- 1 | variable "namespace" { 2 | type = string 3 | } 4 | variable "environment" { 5 | type = map(string) 6 | default = { TERRAFORM = "true" } 7 | } 8 | variable "global_tags" { 9 | type = map(string) 10 | } 11 | variable "name" { 12 | type = string 13 | } 14 | variable "description" { 15 | type = string 16 | } 17 | variable "timeout" { 18 | type = number 19 | default = 300 20 | } 21 | variable "handler" { 22 | type = string 23 | } 24 | variable "alarm_topic_arn" { 25 | type = string 26 | description = "ARN of SNS Topic, for alarm notifications" 27 | } 28 | 29 | variable "dlq_enabled" { 30 | type = bool 31 | description = "Enable Dead Letter Queues (DLQs) for AWS Lambda functions. If enabled, Cloudwatch alarms are also created to monitor DLQs" 32 | default = false 33 | } 34 | -------------------------------------------------------------------------------- /modules/authentication/outputs.tf: -------------------------------------------------------------------------------- 1 | 2 | output user_pool_id { 3 | value = aws_cognito_user_pool._.id 4 | } 5 | 6 | output user_pool_arn { 7 | value = aws_cognito_user_pool._.arn 8 | } 9 | 10 | output client_id { 11 | value = aws_cognito_user_pool_client._.id 12 | } 13 | 14 | output user_policy_arn { 15 | value = aws_iam_policy.user.arn 16 | } 17 | 18 | output user_role_arn { 19 | value = aws_iam_role.user.arn 20 | } 21 | 22 | output admin_policy_arn { 23 | value = aws_iam_policy.admin.arn 24 | } 25 | 26 | output admin_role_arn { 27 | value = aws_iam_role.admin.arn 28 | } 29 | 30 | output identity_pool_id { 31 | value = aws_cognito_identity_pool._.id 32 | } 33 | 34 | output user_pool_domain { 35 | value = aws_cognito_user_pool_domain._.domain 36 | } 37 | 38 | output user_pool_endpoint { 39 | value = aws_cognito_user_pool._.endpoint 40 | } -------------------------------------------------------------------------------- /pkg/api/mocks/UserDetailer.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | api "github.com/Optum/dce/pkg/api" 7 | events "github.com/aws/aws-lambda-go/events" 8 | 9 | mock "github.com/stretchr/testify/mock" 10 | ) 11 | 12 | // UserDetailer is an autogenerated mock type for the UserDetailer type 13 | type UserDetailer struct { 14 | mock.Mock 15 | } 16 | 17 | // GetUser provides a mock function with given fields: reqCtx 18 | func (_m *UserDetailer) GetUser(reqCtx *events.APIGatewayProxyRequestContext) *api.User { 19 | ret := _m.Called(reqCtx) 20 | 21 | var r0 *api.User 22 | if rf, ok := ret.Get(0).(func(*events.APIGatewayProxyRequestContext) *api.User); ok { 23 | r0 = rf(reqCtx) 24 | } else { 25 | if ret.Get(0) != nil { 26 | r0 = ret.Get(0).(*api.User) 27 | } 28 | } 29 | 30 | return r0 31 | } 32 | -------------------------------------------------------------------------------- /pkg/data/dataiface/lease.go: -------------------------------------------------------------------------------- 1 | // 2 | 3 | package dataiface 4 | 5 | import ( 6 | "github.com/Optum/dce/pkg/lease" 7 | ) 8 | 9 | // LeaseData makes working with the Lease Data Layer easier 10 | type LeaseData interface { 11 | 12 | // Get the Lease record by ID 13 | Get(ID string) (*lease.Lease, error) 14 | 15 | // GetByAccountIDAndPrincipalID gets the Lease record by AccountID and PrincipalID 16 | GetByAccountIDAndPrincipalID(accountID string, principalID string) (*lease.Lease, error) 17 | 18 | List(query *lease.Lease) (*lease.Leases, error) 19 | // List Get a list of leases 20 | // Write the Lease record in DynamoDB 21 | // This is an upsert operation in which the record will either 22 | // be inserted or updated 23 | // prevLastModifiedOn parameter is the original lastModifiedOn 24 | Write(lease *lease.Lease, prevLastModifiedOn *int64) error 25 | } 26 | -------------------------------------------------------------------------------- /pkg/event/eventiface/servicer.go: -------------------------------------------------------------------------------- 1 | // 2 | 3 | package eventiface 4 | 5 | import ( 6 | "github.com/Optum/dce/pkg/account" 7 | "github.com/Optum/dce/pkg/lease" 8 | ) 9 | 10 | // Servicer makes work with the event Hub easier 11 | type Servicer interface { 12 | // AccountCreate publish events 13 | AccountCreate(data *account.Account) error 14 | // AccountDelete publish events 15 | AccountDelete(data *account.Account) error 16 | // AccountUpdate publish events 17 | AccountUpdate(old *account.Account, new *account.Account) error 18 | // AccountReset publish events 19 | AccountReset(data *account.Account) error 20 | // LeaseCreate publish events 21 | LeaseCreate(data *lease.Lease) error 22 | // LeaseEnd publish events 23 | LeaseEnd(data *lease.Lease) error 24 | // LeaseUpdate publish events 25 | LeaseUpdate(old *lease.Lease, new *lease.Lease) error 26 | } 27 | -------------------------------------------------------------------------------- /modules/email_identity.tf: -------------------------------------------------------------------------------- 1 | # Creates SES email entry 2 | resource "aws_ses_email_identity" "from_email_address" { 3 | email = var.budget_notification_from_email 4 | } 5 | 6 | # Send delivery failures and bounces to SNS topic 7 | resource "aws_ses_configuration_set" "ses" { 8 | name = "dce_ses_${var.namespace}" 9 | } 10 | 11 | resource "aws_ses_event_destination" "ses_to_cloudwatch" { 12 | name = "event-destination-cloudwatch-${var.namespace}" 13 | configuration_set_name = aws_ses_configuration_set.ses.name 14 | enabled = true 15 | 16 | matching_types = [ 17 | "bounce", 18 | "reject", 19 | "complaint", 20 | "renderingFailure", 21 | ] 22 | 23 | cloudwatch_destination { 24 | default_value = "Failed_Send" 25 | dimension_name = "RB_SES_FAILURE" 26 | value_source = "emailHeader" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pkg/config/mocks/ConfigurationServiceBuilder.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import config "github.com/Optum/dce/pkg/config" 6 | import mock "github.com/stretchr/testify/mock" 7 | 8 | // ConfigurationServiceBuilder is an autogenerated mock type for the ConfigurationServiceBuilder type 9 | type ConfigurationServiceBuilder struct { 10 | mock.Mock 11 | } 12 | 13 | // WithService provides a mock function with given fields: svc 14 | func (_m *ConfigurationServiceBuilder) WithService(svc interface{}) *config.ConfigurationBuilder { 15 | ret := _m.Called(svc) 16 | 17 | var r0 *config.ConfigurationBuilder 18 | if rf, ok := ret.Get(0).(func(interface{}) *config.ConfigurationBuilder); ok { 19 | r0 = rf(svc) 20 | } else { 21 | if ret.Get(0) != nil { 22 | r0 = ret.Get(0).(*config.ConfigurationBuilder) 23 | } 24 | } 25 | 26 | return r0 27 | } 28 | -------------------------------------------------------------------------------- /modules/lease_auth_lambda.tf: -------------------------------------------------------------------------------- 1 | module "lease_auth_lambda" { 2 | source = "./lambda" 3 | name = "lease_auth-${var.namespace}" 4 | namespace = var.namespace 5 | description = "API /leases/id/auth endpoints" 6 | global_tags = var.global_tags 7 | handler = "lease_auth" 8 | alarm_topic_arn = aws_sns_topic.alarms_topic.arn 9 | 10 | environment = { 11 | DEBUG = "false" 12 | NAMESPACE = var.namespace 13 | AWS_CURRENT_REGION = var.aws_region 14 | ACCOUNT_DB = aws_dynamodb_table.accounts.id 15 | LEASE_DB = aws_dynamodb_table.leases.id 16 | COGNITO_USER_POOL_ID = module.api_gateway_authorizer.user_pool_id 17 | COGNITO_ROLES_ATTRIBUTE_ADMIN_NAME = var.cognito_roles_attribute_admin_name 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /pkg/account/mocks/SingleReader.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import account "github.com/Optum/dce/pkg/account" 6 | import mock "github.com/stretchr/testify/mock" 7 | 8 | // SingleReader is an autogenerated mock type for the SingleReader type 9 | type SingleReader struct { 10 | mock.Mock 11 | } 12 | 13 | // Get provides a mock function with given fields: ID 14 | func (_m *SingleReader) Get(ID string) (*account.Account, error) { 15 | ret := _m.Called(ID) 16 | 17 | var r0 *account.Account 18 | if rf, ok := ret.Get(0).(func(string) *account.Account); ok { 19 | r0 = rf(ID) 20 | } else { 21 | if ret.Get(0) != nil { 22 | r0 = ret.Get(0).(*account.Account) 23 | } 24 | } 25 | 26 | var r1 error 27 | if rf, ok := ret.Get(1).(func(string) error); ok { 28 | r1 = rf(ID) 29 | } else { 30 | r1 = ret.Error(1) 31 | } 32 | 33 | return r0, r1 34 | } 35 | -------------------------------------------------------------------------------- /pkg/lease/mocks/MultipleReader.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import lease "github.com/Optum/dce/pkg/lease" 6 | import mock "github.com/stretchr/testify/mock" 7 | 8 | // MultipleReader is an autogenerated mock type for the MultipleReader type 9 | type MultipleReader struct { 10 | mock.Mock 11 | } 12 | 13 | // List provides a mock function with given fields: _a0 14 | func (_m *MultipleReader) List(_a0 *lease.Lease) (*lease.Leases, error) { 15 | ret := _m.Called(_a0) 16 | 17 | var r0 *lease.Leases 18 | if rf, ok := ret.Get(0).(func(*lease.Lease) *lease.Leases); ok { 19 | r0 = rf(_a0) 20 | } else { 21 | if ret.Get(0) != nil { 22 | r0 = ret.Get(0).(*lease.Leases) 23 | } 24 | } 25 | 26 | var r1 error 27 | if rf, ok := ret.Get(1).(func(*lease.Lease) error); ok { 28 | r1 = rf(_a0) 29 | } else { 30 | r1 = ret.Error(1) 31 | } 32 | 33 | return r0, r1 34 | } 35 | -------------------------------------------------------------------------------- /pkg/lease/mocks/AccountServicer.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import account "github.com/Optum/dce/pkg/account" 6 | 7 | import mock "github.com/stretchr/testify/mock" 8 | 9 | // AccountServicer is an autogenerated mock type for the AccountServicer type 10 | type AccountServicer struct { 11 | mock.Mock 12 | } 13 | 14 | // Reset provides a mock function with given fields: id 15 | func (_m *AccountServicer) Reset(id string) (*account.Account, error) { 16 | ret := _m.Called(id) 17 | 18 | var r0 *account.Account 19 | if rf, ok := ret.Get(0).(func(string) *account.Account); ok { 20 | r0 = rf(id) 21 | } else { 22 | if ret.Get(0) != nil { 23 | r0 = ret.Get(0).(*account.Account) 24 | } 25 | } 26 | 27 | var r1 error 28 | if rf, ok := ret.Get(1).(func(string) error); ok { 29 | r1 = rf(id) 30 | } else { 31 | r1 = ret.Error(1) 32 | } 33 | 34 | return r0, r1 35 | } 36 | -------------------------------------------------------------------------------- /pkg/usage/mocks/MultipleReader.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | import usage "github.com/Optum/dce/pkg/usage" 7 | 8 | // MultipleReader is an autogenerated mock type for the MultipleReader type 9 | type MultipleReader struct { 10 | mock.Mock 11 | } 12 | 13 | // List provides a mock function with given fields: query 14 | func (_m *MultipleReader) List(query *usage.Usage) (*usage.Usages, error) { 15 | ret := _m.Called(query) 16 | 17 | var r0 *usage.Usages 18 | if rf, ok := ret.Get(0).(func(*usage.Usage) *usage.Usages); ok { 19 | r0 = rf(query) 20 | } else { 21 | if ret.Get(0) != nil { 22 | r0 = ret.Get(0).(*usage.Usages) 23 | } 24 | } 25 | 26 | var r1 error 27 | if rf, ok := ret.Get(1).(func(*usage.Usage) error); ok { 28 | r1 = rf(query) 29 | } else { 30 | r1 = ret.Error(1) 31 | } 32 | 33 | return r0, r1 34 | } 35 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /pkg/account/mocks/MultipleReader.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import account "github.com/Optum/dce/pkg/account" 6 | import mock "github.com/stretchr/testify/mock" 7 | 8 | // MultipleReader is an autogenerated mock type for the MultipleReader type 9 | type MultipleReader struct { 10 | mock.Mock 11 | } 12 | 13 | // List provides a mock function with given fields: query 14 | func (_m *MultipleReader) List(query *account.Account) (*account.Accounts, error) { 15 | ret := _m.Called(query) 16 | 17 | var r0 *account.Accounts 18 | if rf, ok := ret.Get(0).(func(*account.Account) *account.Accounts); ok { 19 | r0 = rf(query) 20 | } else { 21 | if ret.Get(0) != nil { 22 | r0 = ret.Get(0).(*account.Accounts) 23 | } 24 | } 25 | 26 | var r1 error 27 | if rf, ok := ret.Get(1).(func(*account.Account) error); ok { 28 | r1 = rf(query) 29 | } else { 30 | r1 = ret.Error(1) 31 | } 32 | 33 | return r0, r1 34 | } 35 | -------------------------------------------------------------------------------- /cmd/lambda/accounts/create.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/Optum/dce/pkg/account" 8 | "github.com/Optum/dce/pkg/api" 9 | "github.com/Optum/dce/pkg/errors" 10 | ) 11 | 12 | // CreateAccount - Function to validate the account request to add into the pool and 13 | // publish the account creation to its respective client 14 | func CreateAccount(w http.ResponseWriter, r *http.Request) { 15 | // Deserialize the request JSON as an request object 16 | newAccount := &account.Account{} 17 | decoder := json.NewDecoder(r.Body) 18 | err := decoder.Decode(newAccount) 19 | if err != nil { 20 | api.WriteAPIErrorResponse(w, 21 | errors.NewBadRequest("invalid request parameters")) 22 | return 23 | } 24 | 25 | account, err := Services.AccountService().Create(newAccount) 26 | if err != nil { 27 | api.WriteAPIErrorResponse(w, err) 28 | return 29 | } 30 | 31 | api.WriteAPIResponse(w, http.StatusCreated, account) 32 | } 33 | -------------------------------------------------------------------------------- /pkg/lease/leaseiface/servicer.go: -------------------------------------------------------------------------------- 1 | // 2 | 3 | package leaseiface 4 | 5 | import ( 6 | "github.com/Optum/dce/pkg/lease" 7 | ) 8 | 9 | // Servicer makes working with the Lease Service struct easier 10 | type Servicer interface { 11 | // Get returns an lease from ID 12 | Get(ID string) (*lease.Lease, error) 13 | 14 | // GetByAccountIDAndPrincipalID gets the Lease record by AccountID and PrincipalID 15 | GetByAccountIDAndPrincipalID(accountID string, principalID string) (*lease.Lease, error) 16 | 17 | // Save writes the record to the dataSvc 18 | Create(data *lease.Lease, principalSpentAmount float64) (*lease.Lease, error) 19 | 20 | // Update the Lease record to status Inactive in DynamoDB 21 | Delete(ID string) (*lease.Lease, error) 22 | 23 | // List Get a list of lease based on Lease ID 24 | List(query *lease.Lease) (*lease.Leases, error) 25 | 26 | // ListPages runs a function on each page in a list 27 | ListPages(query *lease.Lease, fn func(*lease.Leases) bool) error 28 | } 29 | -------------------------------------------------------------------------------- /pkg/usage/mocks/SingleReader.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | import usage "github.com/Optum/dce/pkg/usage" 7 | 8 | // SingleReader is an autogenerated mock type for the SingleReader type 9 | type SingleReader struct { 10 | mock.Mock 11 | } 12 | 13 | // Get provides a mock function with given fields: startDate, principalID 14 | func (_m *SingleReader) Get(startDate int64, principalID string) (*usage.Usage, error) { 15 | ret := _m.Called(startDate, principalID) 16 | 17 | var r0 *usage.Usage 18 | if rf, ok := ret.Get(0).(func(int64, string) *usage.Usage); ok { 19 | r0 = rf(startDate, principalID) 20 | } else { 21 | if ret.Get(0) != nil { 22 | r0 = ret.Get(0).(*usage.Usage) 23 | } 24 | } 25 | 26 | var r1 error 27 | if rf, ok := ret.Get(1).(func(int64, string) error); ok { 28 | r1 = rf(startDate, principalID) 29 | } else { 30 | r1 = ret.Error(1) 31 | } 32 | 33 | return r0, r1 34 | } 35 | -------------------------------------------------------------------------------- /pkg/common/mocks/Notificationer.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // Notificationer is an autogenerated mock type for the Notificationer type 8 | type Notificationer struct { 9 | mock.Mock 10 | } 11 | 12 | // PublishMessage provides a mock function with given fields: topicArn, message, isJSON 13 | func (_m *Notificationer) PublishMessage(topicArn *string, message *string, isJSON bool) (*string, error) { 14 | ret := _m.Called(topicArn, message, isJSON) 15 | 16 | var r0 *string 17 | if rf, ok := ret.Get(0).(func(*string, *string, bool) *string); ok { 18 | r0 = rf(topicArn, message, isJSON) 19 | } else { 20 | if ret.Get(0) != nil { 21 | r0 = ret.Get(0).(*string) 22 | } 23 | } 24 | 25 | var r1 error 26 | if rf, ok := ret.Get(1).(func(*string, *string, bool) error); ok { 27 | r1 = rf(topicArn, message, isJSON) 28 | } else { 29 | r1 = ret.Error(1) 30 | } 31 | 32 | return r0, r1 33 | } 34 | -------------------------------------------------------------------------------- /modules/cloudwatch_event/main.tf: -------------------------------------------------------------------------------- 1 | data aws_arn lambda_function { 2 | arn = var.lambda_function_arn 3 | } 4 | 5 | locals { 6 | lambda_function_name = split(":", data.aws_arn.lambda_function.resource)[1] 7 | } 8 | 9 | 10 | resource "aws_cloudwatch_event_rule" "dbbackup" { 11 | count = var.enabled ? 1 : 0 12 | name = var.name 13 | description = var.description 14 | schedule_expression = var.schedule_expression 15 | } 16 | 17 | 18 | resource "aws_cloudwatch_event_target" "dbbackup" { 19 | count = var.enabled ? 1 : 0 20 | arn = var.lambda_function_arn 21 | rule = aws_cloudwatch_event_rule.dbbackup[0].name 22 | target_id = var.name 23 | } 24 | resource "aws_lambda_permission" "dbbackup" { 25 | statement_id = "AllowExecutionFromCloudWatch" 26 | action = "lambda:InvokeFunction" 27 | function_name = local.lambda_function_name 28 | principal = "events.amazonaws.com" 29 | source_arn = aws_cloudwatch_event_rule.dbbackup[0].arn 30 | } -------------------------------------------------------------------------------- /pkg/db/error.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | // StatusTransitionError means that we failed to transition 4 | // an Account or Lease from one status to another, 5 | // likely because the prevStatus condition was not met 6 | type StatusTransitionError struct { 7 | err string 8 | } 9 | 10 | func (e *StatusTransitionError) Error() string { 11 | return e.err 12 | } 13 | 14 | // AccountLeasedError is returned when a consumer attempts to delete an account that is currently at status Leased 15 | type AccountLeasedError struct { 16 | err string 17 | } 18 | 19 | func (e *AccountLeasedError) Error() string { 20 | return e.err 21 | } 22 | 23 | // AccountNotFoundError is returned when an account is not found. 24 | type AccountNotFoundError struct { 25 | err string 26 | } 27 | 28 | func (e *AccountNotFoundError) Error() string { 29 | return e.err 30 | } 31 | 32 | // NotFoundError is returned when a resource is not found. 33 | type NotFoundError struct { 34 | Err string 35 | } 36 | 37 | func (e *NotFoundError) Error() string { 38 | return e.Err 39 | } 40 | -------------------------------------------------------------------------------- /pkg/awsiface/mocks/AwsSession.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import aws "github.com/aws/aws-sdk-go/aws" 6 | 7 | import client "github.com/aws/aws-sdk-go/aws/client" 8 | import mock "github.com/stretchr/testify/mock" 9 | 10 | // AwsSession is an autogenerated mock type for the AwsSession type 11 | type AwsSession struct { 12 | mock.Mock 13 | } 14 | 15 | // ClientConfig provides a mock function with given fields: serviceName, cfgs 16 | func (_m *AwsSession) ClientConfig(serviceName string, cfgs ...*aws.Config) client.Config { 17 | _va := make([]interface{}, len(cfgs)) 18 | for _i := range cfgs { 19 | _va[_i] = cfgs[_i] 20 | } 21 | var _ca []interface{} 22 | _ca = append(_ca, serviceName) 23 | _ca = append(_ca, _va...) 24 | ret := _m.Called(_ca...) 25 | 26 | var r0 client.Config 27 | if rf, ok := ret.Get(0).(func(string, ...*aws.Config) client.Config); ok { 28 | r0 = rf(serviceName, cfgs...) 29 | } else { 30 | r0 = ret.Get(0).(client.Config) 31 | } 32 | 33 | return r0 34 | } 35 | -------------------------------------------------------------------------------- /tools/awsnukedocgen/samples/nuke/resources/iam-users.go.txt: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws/session" 5 | "github.com/aws/aws-sdk-go/service/iam" 6 | ) 7 | 8 | type IAMUser struct { 9 | svc *iam.IAM 10 | name string 11 | } 12 | 13 | func init() { 14 | register("IAMUser", ListIAMUsers) 15 | } 16 | 17 | func ListIAMUsers(sess *session.Session) ([]Resource, error) { 18 | svc := iam.New(sess) 19 | 20 | resp, err := svc.ListUsers(nil) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | resources := make([]Resource, 0) 26 | for _, out := range resp.Users { 27 | resources = append(resources, &IAMUser{ 28 | svc: svc, 29 | name: *out.UserName, 30 | }) 31 | } 32 | 33 | return resources, nil 34 | } 35 | 36 | func (e *IAMUser) Remove() error { 37 | _, err := e.svc.DeleteUser(&iam.DeleteUserInput{ 38 | UserName: &e.name, 39 | }) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | return nil 45 | } 46 | 47 | func (e *IAMUser) String() string { 48 | return e.name 49 | } 50 | -------------------------------------------------------------------------------- /scripts/lint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | echo -n "Formatting golang code... " 5 | gofmtout=$(go fmt ./...) 6 | if [ "$gofmtout" ]; then 7 | printf "\n\n" 8 | echo "Files with formatting errors:" 9 | echo "${gofmtout}" 10 | exit 1 11 | fi 12 | echo "done." 13 | 14 | echo -n "Linting golang code... " 15 | # TODO: Make sure golangci-lint is installed and ready to be run 16 | GOLANG_LINT_CMD=golangci-lint 17 | 18 | if [ ! "$(command -v ${GOLANG_LINT_CMD})" ]; then 19 | echo -n "installing ${GOLANG_LINT_CMD}... " 20 | go get -u github.com/golangci/golangci-lint/cmd/golangci-lint 21 | fi 22 | 23 | golangci-lint run 24 | echo "done." 25 | 26 | gosec ./... 27 | 28 | echo -n "Formatting terraform code.... " 29 | terraform fmt -diff -check -recursive ./modules/ 30 | echo "done." 31 | 32 | # Run tflint 33 | echo -n "Linting terraform code... " 34 | cd modules 35 | terraform init &> /dev/null 36 | # TODO: test to see if tflint is installed first. 37 | tflint --deep ./ | (grep -v "Awesome" || true) 38 | echo -e '\b done.' 39 | -------------------------------------------------------------------------------- /tools/awsnukedocgen/samples/nuke/resources/sns-topics.go.txt: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/aws/aws-sdk-go/aws/session" 7 | "github.com/aws/aws-sdk-go/service/sns" 8 | ) 9 | 10 | func init() { 11 | register("SNSTopic", ListSNSTopics) 12 | } 13 | 14 | func ListSNSTopics(sess *session.Session) ([]Resource, error) { 15 | svc := sns.New(sess) 16 | 17 | resp, err := svc.ListTopics(nil) 18 | if err != nil { 19 | return nil, err 20 | } 21 | resources := make([]Resource, 0) 22 | for _, topic := range resp.Topics { 23 | resources = append(resources, &SNSTopic{ 24 | svc: svc, 25 | id: topic.TopicArn, 26 | }) 27 | } 28 | return resources, nil 29 | } 30 | 31 | type SNSTopic struct { 32 | svc *sns.SNS 33 | id *string 34 | } 35 | 36 | func (topic *SNSTopic) Remove() error { 37 | _, err := topic.svc.DeleteTopic(&sns.DeleteTopicInput{ 38 | TopicArn: topic.id, 39 | }) 40 | return err 41 | } 42 | 43 | func (topic *SNSTopic) String() string { 44 | return fmt.Sprintf("TopicARN: %s", *topic.id) 45 | } 46 | -------------------------------------------------------------------------------- /modules/credentials_web_page_lambda.tf: -------------------------------------------------------------------------------- 1 | module "credentials_web_page_lambda" { 2 | source = "./lambda" 3 | name = "credentials_web_page-${var.namespace}" 4 | namespace = var.namespace 5 | description = "Handles API requests to the /credentials_web_page endpoint" 6 | global_tags = var.global_tags 7 | handler = "credentials_web_page" 8 | alarm_topic_arn = aws_sns_topic.alarms_topic.arn 9 | 10 | environment = { 11 | APIGW_DEPLOYMENT_NAME = "api" 12 | PS_IDENTITY_POOL_ID = module.ssm_parameter_names.identity_pool_id 13 | SITE_PATH_PREFIX = "auth" 14 | PS_USER_POOL_APP_WEB_DOMAIN = module.ssm_parameter_names.user_pool_domain 15 | PS_USER_POOL_CLIENT_ID = module.ssm_parameter_names.client_id 16 | PS_USER_POOL_ID = module.ssm_parameter_names.user_pool_id 17 | PS_USER_POOL_PROVIDER_NAME = module.ssm_parameter_names.user_pool_endpoint 18 | NAMESPACE = var.namespace 19 | AWS_CURRENT_REGION = var.aws_region 20 | } 21 | } -------------------------------------------------------------------------------- /pkg/errors/multierror.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // MultiError is an Error type that wraps multiple errors. 9 | // This can be a useful way to combine errors in a method 10 | // where you want to allow process to continue through multiple 11 | // failed steps. 12 | type MultiError struct { 13 | Message string 14 | Errors []error 15 | } 16 | 17 | // Error returns the error message to satisfy the error interface 18 | func (e MultiError) Error() string { 19 | var errStrs []string 20 | for _, err := range e.Errors { 21 | errStrs = append(errStrs, err.Error()) 22 | } 23 | return fmt.Sprintf( 24 | "%s: %s", 25 | e.Message, 26 | strings.Join(errStrs, "; "), 27 | ) 28 | } 29 | 30 | // Is to satisfy the error comparison interface 31 | func (e MultiError) Is(err error) bool { 32 | return e.Error() == err.Error() 33 | } 34 | 35 | // NewMultiError is a list of errors 36 | func NewMultiError(msg string, errs []error) *MultiError { 37 | return &MultiError{ 38 | Message: msg, 39 | Errors: errs, 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/acceptance/outputs_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "regexp" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/gruntwork-io/terratest/modules/terraform" 10 | ) 11 | 12 | func TestTerraformOutputs(t *testing.T) { 13 | tfOpts := &terraform.Options{ 14 | TerraformDir: "../../modules", 15 | } 16 | 17 | tfOut := terraform.OutputAll(t, tfOpts) 18 | 19 | assert.Regexp(t, 20 | regexp.MustCompile("^Accounts"), 21 | tfOut["accounts_table_name"].(string), 22 | "account_db_table_name", 23 | ) 24 | assert.Regexp(t, 25 | regexp.MustCompile(`^Leases`), 26 | tfOut["leases_table_name"].(string), 27 | "leases_table_name", 28 | ) 29 | assert.Regexp(t, 30 | regexp.MustCompile(`^https:\/\/sqs\.us-east-1\.amazonaws\.com\/[0-9]+\/account-reset-`), 31 | tfOut["sqs_reset_queue_url"].(string), 32 | "sqs_reset_queue_url", 33 | ) 34 | assert.Regexp(t, 35 | regexp.MustCompile(`^arn:aws:sqs:us-east-1:[0-9]+:account-reset-`), 36 | tfOut["sqs_reset_queue_arn"].(string), 37 | "sqs_reset_queue_arn", 38 | ) 39 | 40 | } 41 | -------------------------------------------------------------------------------- /tests/testutils/mocks.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import "github.com/stretchr/testify/mock" 4 | 5 | // ReplaceMock allows you to mock out a method, 6 | // overriding any previous expectations for that same method. 7 | // 8 | // This is useful if you want to setup a generic stub object 9 | // to reuse among multiple tests, but then tweak expectations 10 | // for some of the tests. 11 | // 12 | // Note that you need to pass the underlying mock object to this method, 13 | // as `mock.Mock` does not expose any useful interfaces 14 | // eg. 15 | // 16 | // ReplaceMock(&myMock.Mock, "DoAThing", mock.Anything).Return(nil) 17 | func ReplaceMock(m *mock.Mock, methodName string, arguments ...interface{}) *mock.Call { 18 | // Recreated the list of expected calls, 19 | // excluding calls for this method 20 | var expectedCalls []*mock.Call 21 | for _, call := range m.ExpectedCalls { 22 | if call.Method != methodName { 23 | expectedCalls = append(expectedCalls, call) 24 | } 25 | } 26 | m.ExpectedCalls = expectedCalls 27 | 28 | return m.On(methodName, arguments...) 29 | } 30 | -------------------------------------------------------------------------------- /tools/awsnukedocgen/samples/nuke/resources/sqs-queues.go.txt: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws/session" 5 | "github.com/aws/aws-sdk-go/service/sqs" 6 | ) 7 | 8 | type SQSQueue struct { 9 | svc *sqs.SQS 10 | queueURL *string 11 | } 12 | 13 | func init() { 14 | register("SQSQueue", ListSQSQueues) 15 | } 16 | 17 | func ListSQSQueues(sess *session.Session) ([]Resource, error) { 18 | svc := sqs.New(sess) 19 | 20 | params := &sqs.ListQueuesInput{} 21 | resp, err := svc.ListQueues(params) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | resources := make([]Resource, 0) 27 | for _, queue := range resp.QueueUrls { 28 | resources = append(resources, &SQSQueue{ 29 | svc: svc, 30 | queueURL: queue, 31 | }) 32 | } 33 | 34 | return resources, nil 35 | } 36 | 37 | func (f *SQSQueue) Remove() error { 38 | 39 | _, err := f.svc.DeleteQueue(&sqs.DeleteQueueInput{ 40 | QueueUrl: f.queueURL, 41 | }) 42 | 43 | return err 44 | } 45 | 46 | func (f *SQSQueue) String() string { 47 | return *f.queueURL 48 | } 49 | -------------------------------------------------------------------------------- /pkg/api/mocks/Controller.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import context "context" 6 | import events "github.com/aws/aws-lambda-go/events" 7 | import mock "github.com/stretchr/testify/mock" 8 | 9 | // Controller is an autogenerated mock type for the Controller type 10 | type Controller struct { 11 | mock.Mock 12 | } 13 | 14 | // Call provides a mock function with given fields: ctx, req 15 | func (_m *Controller) Call(ctx context.Context, req *events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 16 | ret := _m.Called(ctx, req) 17 | 18 | var r0 events.APIGatewayProxyResponse 19 | if rf, ok := ret.Get(0).(func(context.Context, *events.APIGatewayProxyRequest) events.APIGatewayProxyResponse); ok { 20 | r0 = rf(ctx, req) 21 | } else { 22 | r0 = ret.Get(0).(events.APIGatewayProxyResponse) 23 | } 24 | 25 | var r1 error 26 | if rf, ok := ret.Get(1).(func(context.Context, *events.APIGatewayProxyRequest) error); ok { 27 | r1 = rf(ctx, req) 28 | } else { 29 | r1 = ret.Error(1) 30 | } 31 | 32 | return r0, r1 33 | } 34 | -------------------------------------------------------------------------------- /pkg/account/mocks/WriterDeleter.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import account "github.com/Optum/dce/pkg/account" 6 | import mock "github.com/stretchr/testify/mock" 7 | 8 | // WriterDeleter is an autogenerated mock type for the WriterDeleter type 9 | type WriterDeleter struct { 10 | mock.Mock 11 | } 12 | 13 | // Delete provides a mock function with given fields: i 14 | func (_m *WriterDeleter) Delete(i *account.Account) error { 15 | ret := _m.Called(i) 16 | 17 | var r0 error 18 | if rf, ok := ret.Get(0).(func(*account.Account) error); ok { 19 | r0 = rf(i) 20 | } else { 21 | r0 = ret.Error(0) 22 | } 23 | 24 | return r0 25 | } 26 | 27 | // Write provides a mock function with given fields: i, lastModifiedOn 28 | func (_m *WriterDeleter) Write(i *account.Account, lastModifiedOn *int64) error { 29 | ret := _m.Called(i, lastModifiedOn) 30 | 31 | var r0 error 32 | if rf, ok := ret.Get(0).(func(*account.Account, *int64) error); ok { 33 | r0 = rf(i, lastModifiedOn) 34 | } else { 35 | r0 = ret.Error(0) 36 | } 37 | 38 | return r0 39 | } 40 | -------------------------------------------------------------------------------- /pkg/email/mocks/Service.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import email "github.com/Optum/dce/pkg/email" 6 | import mock "github.com/stretchr/testify/mock" 7 | 8 | // Service is an autogenerated mock type for the Service type 9 | type Service struct { 10 | mock.Mock 11 | } 12 | 13 | // SendEmail provides a mock function with given fields: input 14 | func (_m *Service) SendEmail(input *email.SendEmailInput) error { 15 | ret := _m.Called(input) 16 | 17 | var r0 error 18 | if rf, ok := ret.Get(0).(func(*email.SendEmailInput) error); ok { 19 | r0 = rf(input) 20 | } else { 21 | r0 = ret.Error(0) 22 | } 23 | 24 | return r0 25 | } 26 | 27 | // SendRawEmailWithAttachment provides a mock function with given fields: input 28 | func (_m *Service) SendRawEmailWithAttachment(input *email.SendEmailWithAttachmentInput) error { 29 | ret := _m.Called(input) 30 | 31 | var r0 error 32 | if rf, ok := ret.Get(0).(func(*email.SendEmailWithAttachmentInput) error); ok { 33 | r0 = rf(input) 34 | } else { 35 | r0 = ret.Error(0) 36 | } 37 | 38 | return r0 39 | } 40 | -------------------------------------------------------------------------------- /pkg/usage/validate.go: -------------------------------------------------------------------------------- 1 | package usage 2 | 3 | import ( 4 | "regexp" 5 | 6 | validation "github.com/go-ozzo/ozzo-validation" 7 | ) 8 | 9 | // We don't use the internal errors package here because validation will rewrite it anyways 10 | // Just spit out errors and turn them into validation errors inside the appropriate functions 11 | 12 | var validatePrincipalID = []validation.Rule{ 13 | validation.NotNil.Error("must be a valid principal ID"), 14 | } 15 | 16 | var validateAccountID = []validation.Rule{ 17 | validation.NotNil.Error("must be a string"), 18 | validation.Match(regexp.MustCompile("^[0-9]{12}$")).Error("must be a string with 12 digits"), 19 | } 20 | 21 | var validateInt64 = []validation.Rule{ 22 | validation.NotNil.Error("must be an epoch timestamp"), 23 | } 24 | 25 | var validateFloat64 = []validation.Rule{ 26 | validation.NotNil.Error("must be a valid cost amount"), 27 | } 28 | 29 | var validateCostCurrency = []validation.Rule{ 30 | validation.NotNil.Error("must be a valid cost concurrency"), 31 | } 32 | 33 | var validateTimeToLive = []validation.Rule{ 34 | validation.NotNil.Error("must be a valid time to live"), 35 | } 36 | -------------------------------------------------------------------------------- /cmd/lambda/accounts/list.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/Optum/dce/pkg/account" 8 | "github.com/Optum/dce/pkg/api" 9 | "github.com/Optum/dce/pkg/api/response" 10 | "github.com/gorilla/schema" 11 | ) 12 | 13 | // GetAccounts - Returns accounts 14 | func GetAccounts(w http.ResponseWriter, r *http.Request) { 15 | // Fetch the accounts. 16 | 17 | var decoder = schema.NewDecoder() 18 | 19 | query := &account.Account{} 20 | err := decoder.Decode(query, r.URL.Query()) 21 | if err != nil { 22 | response.WriteRequestValidationError(w, fmt.Sprintf("Error parsing query params: %s", err)) 23 | return 24 | } 25 | 26 | accounts, err := Services.AccountService().List(query) 27 | if err != nil { 28 | api.WriteAPIErrorResponse(w, err) 29 | return 30 | } 31 | 32 | if query.NextID != nil { 33 | nextURL, err := api.BuildNextURL(baseRequest, query) 34 | if err != nil { 35 | api.WriteAPIErrorResponse(w, err) 36 | return 37 | } 38 | w.Header().Add("Link", fmt.Sprintf("<%s>; rel=\"next\"", nextURL.String())) 39 | } 40 | api.WriteAPIResponse(w, http.StatusOK, accounts) 41 | 42 | } 43 | -------------------------------------------------------------------------------- /pkg/api/error.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/Optum/dce/pkg/errors" 9 | ) 10 | 11 | // WriteAPIErrorResponse writes an error to the ResponseWriter 12 | func WriteAPIErrorResponse(w http.ResponseWriter, err error) { 13 | if debug { 14 | log.Printf("%+v", err) 15 | } else { 16 | log.Printf("%v", err) 17 | } 18 | 19 | switch t := err.(type) { 20 | case errors.HTTPCode: 21 | WriteAPIResponse(w, t.HTTPCode(), err) 22 | return 23 | } 24 | WriteAPIResponse( 25 | w, 26 | http.StatusInternalServerError, 27 | errors.NewInternalServer("unknown error", err), 28 | ) 29 | } 30 | 31 | // WriteAPIResponse writes the response out to the provided ResponseWriter 32 | func WriteAPIResponse(w http.ResponseWriter, status int, body interface{}) { 33 | w.WriteHeader(status) 34 | if body != nil { 35 | err := json.NewEncoder(w).Encode(body) 36 | if err != nil { 37 | log.Printf("error encoding and writing message: %+v", body) 38 | w.WriteHeader(http.StatusInternalServerError) 39 | _ = json.NewEncoder(w).Encode(errors.NewInternalServer("error writing response", err)) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /pkg/usage/validate_test.go: -------------------------------------------------------------------------------- 1 | package usage_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/Optum/dce/pkg/errors" 8 | "github.com/Optum/dce/pkg/usage" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestValidate(t *testing.T) { 13 | tests := []struct { 14 | name string 15 | expErr error 16 | usage usage.NewUsageInput 17 | }{ 18 | { 19 | name: "should validate", 20 | usage: usage.NewUsageInput{ 21 | StartDate: 1580924093, 22 | AccountID: "123456789012", 23 | PrincipalID: "user1", 24 | }, 25 | }, 26 | { 27 | name: "should not validate no status", 28 | usage: usage.NewUsageInput{ 29 | StartDate: 1580924093, 30 | AccountID: "ABCADF", 31 | PrincipalID: "user1", 32 | }, 33 | expErr: errors.NewValidation("usage", fmt.Errorf("accountId: must be a string with 12 digits.")), //nolint golint 34 | }, 35 | } 36 | 37 | for _, tt := range tests { 38 | t.Run(tt.name, func(t *testing.T) { 39 | 40 | _, err := usage.NewUsage(tt.usage) 41 | assert.True(t, errors.Is(err, tt.expErr), "actual error %q doesn't match expected error %q", err, tt.expErr) 42 | 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /pkg/budget/mocks/Service.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import awsiface "github.com/Optum/dce/pkg/awsiface" 6 | 7 | import mock "github.com/stretchr/testify/mock" 8 | import time "time" 9 | 10 | // Service is an autogenerated mock type for the Service type 11 | type Service struct { 12 | mock.Mock 13 | } 14 | 15 | // CalculateTotalSpend provides a mock function with given fields: startDate, endDate 16 | func (_m *Service) CalculateTotalSpend(startDate time.Time, endDate time.Time) (float64, error) { 17 | ret := _m.Called(startDate, endDate) 18 | 19 | var r0 float64 20 | if rf, ok := ret.Get(0).(func(time.Time, time.Time) float64); ok { 21 | r0 = rf(startDate, endDate) 22 | } else { 23 | r0 = ret.Get(0).(float64) 24 | } 25 | 26 | var r1 error 27 | if rf, ok := ret.Get(1).(func(time.Time, time.Time) error); ok { 28 | r1 = rf(startDate, endDate) 29 | } else { 30 | r1 = ret.Error(1) 31 | } 32 | 33 | return r0, r1 34 | } 35 | 36 | // SetCostExplorer provides a mock function with given fields: costExplorer 37 | func (_m *Service) SetCostExplorer(costExplorer awsiface.CostExplorerAPI) { 38 | _m.Called(costExplorer) 39 | } 40 | -------------------------------------------------------------------------------- /pkg/event/sqs.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/Optum/dce/pkg/errors" 7 | "github.com/aws/aws-sdk-go/aws" 8 | "github.com/aws/aws-sdk-go/service/sqs" 9 | "github.com/aws/aws-sdk-go/service/sqs/sqsiface" 10 | ) 11 | 12 | // SqsEvent is for publishing events to SQS 13 | type SqsEvent struct { 14 | sqs sqsiface.SQSAPI 15 | url string 16 | } 17 | 18 | // Publish an event to the topic 19 | func (s *SqsEvent) Publish(i interface{}) error { 20 | bodyJSON, err := json.Marshal(i) 21 | if err != nil { 22 | return errors.NewInternalServer("unable to marshal response", err) 23 | } 24 | 25 | // Create the input 26 | input := sqs.SendMessageInput{ 27 | QueueUrl: aws.String(s.url), 28 | MessageBody: aws.String(string(bodyJSON)), 29 | } 30 | 31 | // Send the message 32 | _, err = s.sqs.SendMessage(&input) 33 | if err != nil { 34 | return errors.NewInternalServer("unable to send message to sqs", err) 35 | } 36 | return nil 37 | } 38 | 39 | // NewSqsEvent creates a new SQS eventing struct 40 | func NewSqsEvent(sqs sqsiface.SQSAPI, url string) (*SqsEvent, error) { 41 | 42 | return &SqsEvent{ 43 | sqs: sqs, 44 | url: url, 45 | }, nil 46 | } 47 | -------------------------------------------------------------------------------- /cmd/codebuild/reset/default-nuke-config-template.yml: -------------------------------------------------------------------------------- 1 | regions: 2 | - "global" 3 | # DCE Principals roles are currently locked down 4 | # to only access these two regions 5 | # This significantly reduces the run time of nuke. 6 | {{range .Regions}} - "{{.}}" 7 | {{end}} 8 | account-blacklist: 9 | - "{{ .ParentAccountID}}" # Arbitrary production account id 10 | 11 | resource-types: 12 | excludes: 13 | - S3Object # Let the S3Bucket delete all Objects instead of individual objects (optimization) 14 | 15 | accounts: 16 | "{{ .ID}}": # Child Account 17 | filters: 18 | IAMPolicy: 19 | - type: "contains" 20 | value: "{{ .PrincipalPolicy}}" 21 | IAMRole: 22 | - "{{ .AdminRole}}" 23 | - "{{ .PrincipalRole}}" 24 | IAMRolePolicy: 25 | - type: "contains" 26 | value: "{{ .AdminRole}}" 27 | - type: "contains" 28 | value: "{{ .PrincipalRole}}" 29 | - type: "contains" 30 | value: "{{ .PrincipalPolicy}}" 31 | IAMRolePolicyAttachment: 32 | # Do not remove the policy from the principal user role 33 | - "{{ .PrincipalRole}} -> {{ .PrincipalPolicy}}" 34 | - property: RoleName 35 | value: "{{ .AdminRole}}" 36 | -------------------------------------------------------------------------------- /pkg/accountmanager/mocks/clienter.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import arn "github.com/Optum/dce/pkg/arn" 6 | import aws "github.com/aws/aws-sdk-go/aws" 7 | import iamiface "github.com/aws/aws-sdk-go/service/iam/iamiface" 8 | import mock "github.com/stretchr/testify/mock" 9 | 10 | // Clienter is an autogenerated mock type for the clienter type 11 | type Clienter struct { 12 | mock.Mock 13 | } 14 | 15 | // Config provides a mock function with given fields: roleArn 16 | func (_m *Clienter) Config(roleArn *arn.ARN) *aws.Config { 17 | ret := _m.Called(roleArn) 18 | 19 | var r0 *aws.Config 20 | if rf, ok := ret.Get(0).(func(*arn.ARN) *aws.Config); ok { 21 | r0 = rf(roleArn) 22 | } else { 23 | if ret.Get(0) != nil { 24 | r0 = ret.Get(0).(*aws.Config) 25 | } 26 | } 27 | 28 | return r0 29 | } 30 | 31 | // IAM provides a mock function with given fields: roleArn 32 | func (_m *Clienter) IAM(roleArn *arn.ARN) iamiface.IAMAPI { 33 | ret := _m.Called(roleArn) 34 | 35 | var r0 iamiface.IAMAPI 36 | if rf, ok := ret.Get(0).(func(*arn.ARN) iamiface.IAMAPI); ok { 37 | r0 = rf(roleArn) 38 | } else { 39 | if ret.Get(0) != nil { 40 | r0 = ret.Get(0).(iamiface.IAMAPI) 41 | } 42 | } 43 | 44 | return r0 45 | } 46 | -------------------------------------------------------------------------------- /tools/awsnukedocgen/samples/nuke/resources/glue-jobs.go.txt: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws" 5 | "github.com/aws/aws-sdk-go/aws/session" 6 | "github.com/aws/aws-sdk-go/service/glue" 7 | ) 8 | 9 | type GlueJob struct { 10 | svc *glue.Glue 11 | jobName *string 12 | } 13 | 14 | func init() { 15 | register("GlueJob", ListGlueJobs) 16 | } 17 | 18 | func ListGlueJobs(sess *session.Session) ([]Resource, error) { 19 | svc := glue.New(sess) 20 | resources := []Resource{} 21 | 22 | params := &glue.GetJobsInput{ 23 | MaxResults: aws.Int64(100), 24 | } 25 | 26 | for { 27 | output, err := svc.GetJobs(params) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | for _, job := range output.Jobs { 33 | resources = append(resources, &GlueJob{ 34 | svc: svc, 35 | jobName: job.Name, 36 | }) 37 | } 38 | 39 | if output.NextToken == nil { 40 | break 41 | } 42 | 43 | params.NextToken = output.NextToken 44 | } 45 | 46 | return resources, nil 47 | } 48 | 49 | func (f *GlueJob) Remove() error { 50 | 51 | _, err := f.svc.DeleteJob(&glue.DeleteJobInput{ 52 | JobName: f.jobName, 53 | }) 54 | 55 | return err 56 | } 57 | 58 | func (f *GlueJob) String() string { 59 | return *f.jobName 60 | } 61 | -------------------------------------------------------------------------------- /tools/awsnukedocgen/samples/nuke/resources/mq-broker.go.txt: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws" 5 | "github.com/aws/aws-sdk-go/aws/session" 6 | "github.com/aws/aws-sdk-go/service/mq" 7 | ) 8 | 9 | type MQBroker struct { 10 | svc *mq.MQ 11 | brokerID *string 12 | } 13 | 14 | func init() { 15 | register("MQBroker", ListMQBrokers) 16 | } 17 | 18 | func ListMQBrokers(sess *session.Session) ([]Resource, error) { 19 | svc := mq.New(sess) 20 | resources := []Resource{} 21 | 22 | params := &mq.ListBrokersInput{ 23 | MaxResults: aws.Int64(100), 24 | } 25 | 26 | for { 27 | resp, err := svc.ListBrokers(params) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | for _, broker := range resp.BrokerSummaries { 33 | resources = append(resources, &MQBroker{ 34 | svc: svc, 35 | brokerID: broker.BrokerId, 36 | }) 37 | } 38 | if resp.NextToken == nil { 39 | break 40 | } 41 | 42 | params.NextToken = resp.NextToken 43 | } 44 | return resources, nil 45 | } 46 | 47 | func (f *MQBroker) Remove() error { 48 | 49 | _, err := f.svc.DeleteBroker(&mq.DeleteBrokerInput{ 50 | BrokerId: f.brokerID, 51 | }) 52 | 53 | return err 54 | } 55 | 56 | func (f *MQBroker) String() string { 57 | return *f.brokerID 58 | } 59 | -------------------------------------------------------------------------------- /pkg/account/accountiface/servicer.go: -------------------------------------------------------------------------------- 1 | // 2 | 3 | package accountiface 4 | 5 | import ( 6 | "github.com/Optum/dce/pkg/account" 7 | ) 8 | 9 | // Servicer makes working with the Account Service struct easier 10 | type Servicer interface { 11 | // Get returns an account from ID 12 | Get(ID string) (*account.Account, error) 13 | // Save writes the record to the dataSvc 14 | Save(data *account.Account) error 15 | // Update the Account record in DynamoDB 16 | Update(ID string, data *account.Account) (*account.Account, error) 17 | // Delete finds a given account and deletes it if it is not of status `Leased`. Returns the account. 18 | Delete(data *account.Account) error 19 | // List Get a list of accounts based on Principal ID 20 | List(query *account.Account) (*account.Accounts, error) 21 | // ListPages Execute a function per page of accounts 22 | ListPages(query *account.Account, fn func(*account.Accounts) bool) error 23 | // Create creates a new account using the data provided. Returns the account record 24 | Create(data *account.Account) (*account.Account, error) 25 | // Reset initiates the Reset account process. 26 | Reset(id string) (*account.Account, error) 27 | // UpsertPrincipalAccess merges principal access to make sure its 28 | UpsertPrincipalAccess(data *account.Account) error 29 | } 30 | -------------------------------------------------------------------------------- /pkg/api/response/account.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "github.com/Optum/dce/pkg/db" 5 | ) 6 | 7 | // AccountResponse is the serialized JSON Response for an account 8 | // to be returned for APIs 9 | // { 10 | // "id": "123", 11 | // "status": "Active", 12 | // "lastModifiedOn": 56789, 13 | // "createOn": 12345, 14 | // "adminRoleArn": " arn:aws:iam::1234567890:role/adminRole 15 | // } 16 | // 17 | // Converting from a db.Account can be done via type casting: 18 | // dbAccount := db.Account{...} 19 | // accountRes := response.AccountResponse(dbAccount) 20 | type AccountResponse struct { 21 | ID string `json:"id"` 22 | AccountStatus db.AccountStatus `json:"accountStatus"` 23 | LastModifiedOn int64 `json:"lastModifiedOn"` 24 | CreatedOn int64 `json:"createdOn"` 25 | AdminRoleArn string `json:"adminRoleArn"` // Assumed by the master account, to manage this user account 26 | PrincipalRoleArn string `json:"principalRoleArn"` // Assumed by principal users 27 | PrincipalPolicyHash string `json:"principalPolicyHash"` // The policy used by the PrincipalRoleArn 28 | Metadata map[string]interface{} `json:"metadata"` 29 | } 30 | -------------------------------------------------------------------------------- /tools/awsnukedocgen/samples/nuke/resources/iot-jobs.go.txt: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws" 5 | "github.com/aws/aws-sdk-go/aws/session" 6 | "github.com/aws/aws-sdk-go/service/iot" 7 | ) 8 | 9 | type IoTJob struct { 10 | svc *iot.IoT 11 | ID *string 12 | status *string 13 | } 14 | 15 | func init() { 16 | register("IoTJob", ListIoTJobs) 17 | } 18 | 19 | func ListIoTJobs(sess *session.Session) ([]Resource, error) { 20 | svc := iot.New(sess) 21 | resources := []Resource{} 22 | 23 | params := &iot.ListJobsInput{ 24 | MaxResults: aws.Int64(100), 25 | Status: aws.String("IN_PROGRESS"), 26 | } 27 | for { 28 | output, err := svc.ListJobs(params) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | for _, job := range output.Jobs { 34 | resources = append(resources, &IoTJob{ 35 | svc: svc, 36 | ID: job.JobId, 37 | status: job.Status, 38 | }) 39 | } 40 | if output.NextToken == nil { 41 | break 42 | } 43 | 44 | params.NextToken = output.NextToken 45 | } 46 | 47 | return resources, nil 48 | } 49 | 50 | func (f *IoTJob) Remove() error { 51 | 52 | _, err := f.svc.CancelJob(&iot.CancelJobInput{ 53 | JobId: f.ID, 54 | }) 55 | 56 | return err 57 | } 58 | 59 | func (f *IoTJob) String() string { 60 | return *f.ID 61 | } 62 | -------------------------------------------------------------------------------- /tests/testutils/retry_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package testutils 16 | 17 | import ( 18 | "testing" 19 | "time" 20 | ) 21 | 22 | func TestRetry(t *testing.T) { 23 | Retry(t, 5, time.Millisecond, func(r *R) { 24 | if r.Attempt == 2 { 25 | return 26 | } 27 | r.Fail() 28 | }) 29 | } 30 | 31 | func TestRetryAttempts(t *testing.T) { 32 | var attempts int 33 | Retry(t, 10, time.Millisecond, func(r *R) { 34 | r.Logf("This line should appear only once.") 35 | r.Logf("attempt=%d", r.Attempt) 36 | attempts = r.Attempt 37 | 38 | // Retry 5 times. 39 | if r.Attempt == 5 { 40 | return 41 | } 42 | r.Fail() 43 | }) 44 | 45 | if attempts != 5 { 46 | t.Errorf("attempts=%d; want %d", attempts, 5) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tools/awsnukedocgen/samples/nuke/resources/batch-jobqueues.go.txt: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws" 5 | "github.com/aws/aws-sdk-go/aws/session" 6 | "github.com/aws/aws-sdk-go/service/batch" 7 | ) 8 | 9 | type BatchJobQueue struct { 10 | svc *batch.Batch 11 | jobQueue *string 12 | } 13 | 14 | func init() { 15 | register("BatchJobQueue", ListBatchJobQueues) 16 | } 17 | 18 | func ListBatchJobQueues(sess *session.Session) ([]Resource, error) { 19 | svc := batch.New(sess) 20 | resources := []Resource{} 21 | 22 | params := &batch.DescribeJobQueuesInput{ 23 | MaxResults: aws.Int64(100), 24 | } 25 | 26 | for { 27 | output, err := svc.DescribeJobQueues(params) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | for _, queue := range output.JobQueues { 33 | resources = append(resources, &BatchJobQueue{ 34 | svc: svc, 35 | jobQueue: queue.JobQueueName, 36 | }) 37 | } 38 | 39 | if output.NextToken == nil { 40 | break 41 | } 42 | 43 | params.NextToken = output.NextToken 44 | } 45 | 46 | return resources, nil 47 | } 48 | 49 | func (f *BatchJobQueue) Remove() error { 50 | 51 | _, err := f.svc.DeleteJobQueue(&batch.DeleteJobQueueInput{ 52 | JobQueue: f.jobQueue, 53 | }) 54 | 55 | return err 56 | } 57 | 58 | func (f *BatchJobQueue) String() string { 59 | return *f.jobQueue 60 | } 61 | -------------------------------------------------------------------------------- /pkg/config/configiface/servicebuilder.go: -------------------------------------------------------------------------------- 1 | // 2 | 3 | package configiface 4 | 5 | import "github.com/Optum/dce/pkg/config" 6 | 7 | // ServiceBuilder makes working with the ServiceBuild easier 8 | type ServiceBuilder interface { 9 | // WithSTS tells the builder to add an AWS STS service to the `DefaultConfigurater` 10 | WithSTS() *config.ServiceBuilder 11 | // WithSNS tells the builder to add an AWS SNS service to the `DefaultConfigurater` 12 | WithSNS() *config.ServiceBuilder 13 | // WithSQS tells the builder to add an AWS SQS service to the `DefaultConfigurater` 14 | WithSQS() *config.ServiceBuilder 15 | // WithDynamoDB tells the builder to add an AWS DynamoDB service to the `DefaultConfigurater` 16 | WithDynamoDB() *config.ServiceBuilder 17 | // WithS3 tells the builder to add an AWS S3 service to the `DefaultConfigurater` 18 | WithS3() *config.ServiceBuilder 19 | // WithCognito tells the builder to add an AWS Cognito service to the `DefaultConfigurater` 20 | WithCognito() *config.ServiceBuilder 21 | // WithCodeBuild tells the builder to add an AWS CodeBuild service to the `DefaultConfigurater` 22 | WithCodeBuild() *config.ServiceBuilder 23 | // WithSSM tells the builder to add an AWS SSM service to the `DefaultConfigurater` 24 | WithSSM() *config.ServiceBuilder 25 | // Build creates and returns a structue with AWS services 26 | Build() (*config.ConfigurationBuilder, error) 27 | } 28 | -------------------------------------------------------------------------------- /tools/awsnukedocgen/samples/nuke/resources/autoscaling-groups.go.txt: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws" 5 | "github.com/aws/aws-sdk-go/aws/session" 6 | "github.com/aws/aws-sdk-go/service/autoscaling" 7 | ) 8 | 9 | func init() { 10 | register("AutoScalingGroup", ListAutoscalingGroups) 11 | } 12 | 13 | func ListAutoscalingGroups(s *session.Session) ([]Resource, error) { 14 | svc := autoscaling.New(s) 15 | 16 | params := &autoscaling.DescribeAutoScalingGroupsInput{} 17 | resp, err := svc.DescribeAutoScalingGroups(params) 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | resources := make([]Resource, 0) 23 | for _, asg := range resp.AutoScalingGroups { 24 | resources = append(resources, &AutoScalingGroup{ 25 | svc: svc, 26 | name: asg.AutoScalingGroupName, 27 | }) 28 | } 29 | return resources, nil 30 | } 31 | 32 | type AutoScalingGroup struct { 33 | svc *autoscaling.AutoScaling 34 | name *string 35 | } 36 | 37 | func (asg *AutoScalingGroup) Remove() error { 38 | params := &autoscaling.DeleteAutoScalingGroupInput{ 39 | AutoScalingGroupName: asg.name, 40 | ForceDelete: aws.Bool(true), 41 | } 42 | 43 | _, err := asg.svc.DeleteAutoScalingGroup(params) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | return nil 49 | } 50 | 51 | func (asg *AutoScalingGroup) String() string { 52 | return *asg.name 53 | } 54 | -------------------------------------------------------------------------------- /cmd/lambda/leases/list.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/Optum/dce/pkg/api" 6 | "github.com/Optum/dce/pkg/api/response" 7 | "github.com/Optum/dce/pkg/lease" 8 | "github.com/gorilla/schema" 9 | "net/http" 10 | ) 11 | 12 | // GetLeases - Returns leases 13 | func GetLeases(w http.ResponseWriter, r *http.Request) { 14 | // Fetch the leases. 15 | 16 | var decoder = schema.NewDecoder() 17 | 18 | query := &lease.Lease{} 19 | err := decoder.Decode(query, r.URL.Query()) 20 | if err != nil { 21 | response.WriteRequestValidationError(w, fmt.Sprintf("Error parsing query params: %s", err)) 22 | return 23 | } 24 | // If user is not an admin, they may only list their own leases 25 | user := r.Context().Value(api.User{}).(*api.User) 26 | if user.Role != api.AdminGroupName { 27 | usersPrincipalID := user.Username 28 | query.PrincipalID = &usersPrincipalID 29 | } 30 | 31 | leases, err := Services.LeaseService().List(query) 32 | if err != nil { 33 | api.WriteAPIErrorResponse(w, err) 34 | return 35 | } 36 | 37 | if query.NextAccountID != nil && query.NextPrincipalID != nil { 38 | nextURL, err := api.BuildNextURL(baseRequest, query) 39 | if err != nil { 40 | api.WriteAPIErrorResponse(w, err) 41 | return 42 | } 43 | w.Header().Add("Link", fmt.Sprintf("<%s>; rel=\"next\"", nextURL.String())) 44 | } 45 | api.WriteAPIResponse(w, http.StatusOK, leases) 46 | 47 | } 48 | -------------------------------------------------------------------------------- /pkg/lease/mocks/Eventer.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import lease "github.com/Optum/dce/pkg/lease" 6 | import mock "github.com/stretchr/testify/mock" 7 | 8 | // Eventer is an autogenerated mock type for the Eventer type 9 | type Eventer struct { 10 | mock.Mock 11 | } 12 | 13 | // LeaseCreate provides a mock function with given fields: account 14 | func (_m *Eventer) LeaseCreate(account *lease.Lease) error { 15 | ret := _m.Called(account) 16 | 17 | var r0 error 18 | if rf, ok := ret.Get(0).(func(*lease.Lease) error); ok { 19 | r0 = rf(account) 20 | } else { 21 | r0 = ret.Error(0) 22 | } 23 | 24 | return r0 25 | } 26 | 27 | // LeaseEnd provides a mock function with given fields: account 28 | func (_m *Eventer) LeaseEnd(account *lease.Lease) error { 29 | ret := _m.Called(account) 30 | 31 | var r0 error 32 | if rf, ok := ret.Get(0).(func(*lease.Lease) error); ok { 33 | r0 = rf(account) 34 | } else { 35 | r0 = ret.Error(0) 36 | } 37 | 38 | return r0 39 | } 40 | 41 | // LeaseUpdate provides a mock function with given fields: old, new 42 | func (_m *Eventer) LeaseUpdate(old *lease.Lease, new *lease.Lease) error { 43 | ret := _m.Called(old, new) 44 | 45 | var r0 error 46 | if rf, ok := ret.Get(0).(func(*lease.Lease, *lease.Lease) error); ok { 47 | r0 = rf(old, new) 48 | } else { 49 | r0 = ret.Error(0) 50 | } 51 | 52 | return r0 53 | } 54 | -------------------------------------------------------------------------------- /tools/awsnukedocgen/samples/nuke/resources/waf-rules.go.txt: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws" 5 | "github.com/aws/aws-sdk-go/aws/session" 6 | "github.com/aws/aws-sdk-go/service/waf" 7 | ) 8 | 9 | type WAFRule struct { 10 | svc *waf.WAF 11 | ID *string 12 | } 13 | 14 | func init() { 15 | register("WAFRule", ListWAFRules) 16 | } 17 | 18 | func ListWAFRules(sess *session.Session) ([]Resource, error) { 19 | svc := waf.New(sess) 20 | resources := []Resource{} 21 | 22 | params := &waf.ListRulesInput{ 23 | Limit: aws.Int64(50), 24 | } 25 | 26 | for { 27 | resp, err := svc.ListRules(params) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | for _, rule := range resp.Rules { 33 | resources = append(resources, &WAFRule{ 34 | svc: svc, 35 | ID: rule.RuleId, 36 | }) 37 | } 38 | 39 | if resp.NextMarker == nil { 40 | break 41 | } 42 | 43 | params.NextMarker = resp.NextMarker 44 | } 45 | 46 | return resources, nil 47 | } 48 | 49 | func (f *WAFRule) Remove() error { 50 | 51 | tokenOutput, err := f.svc.GetChangeToken(&waf.GetChangeTokenInput{}) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | _, err = f.svc.DeleteRule(&waf.DeleteRuleInput{ 57 | RuleId: f.ID, 58 | ChangeToken: tokenOutput.ChangeToken, 59 | }) 60 | 61 | return err 62 | } 63 | 64 | func (f *WAFRule) String() string { 65 | return *f.ID 66 | } 67 | -------------------------------------------------------------------------------- /cmd/lambda/credentials_web_page/get.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/Optum/dce/pkg/api/response" 6 | "html/template" 7 | "log" 8 | "net/http" 9 | "path/filepath" 10 | "strings" 11 | ) 12 | 13 | func GetAuthPage(w http.ResponseWriter, r *http.Request) { 14 | lp := filepath.Join("views", "index.html") 15 | 16 | tmpl, err := template.ParseFiles(lp) 17 | if err != nil { 18 | errorMessage := fmt.Sprintf("Failed to load web page: %s", err) 19 | log.Print(errorMessage) 20 | response.WriteServerErrorWithResponse(w, errorMessage) 21 | } 22 | if err := tmpl.Execute(w, Settings); err != nil { 23 | errorMessage := fmt.Sprintf("Failed to load web page: %s", err) 24 | log.Print(errorMessage) 25 | response.WriteServerErrorWithResponse(w, errorMessage) 26 | } 27 | w.Header().Set("Content-Type", "text/html") 28 | w.WriteHeader(http.StatusOK) 29 | } 30 | 31 | func GetAuthPageAssets(w http.ResponseWriter, r *http.Request) { 32 | fs := http.FileServer(http.Dir("./public")) 33 | sp := http.StripPrefix("/auth/public", fs) 34 | 35 | splitStr := strings.Split(r.URL.Path, ".") 36 | ext := splitStr[len(splitStr)-1] 37 | var contentType string 38 | switch ext { 39 | case "css": 40 | contentType = "text/css" 41 | case "js": 42 | contentType = "text/javascript" 43 | default: 44 | contentType = "application/json" 45 | } 46 | 47 | w.Header().Set("Content-Type", contentType) 48 | sp.ServeHTTP(w, r) 49 | } 50 | -------------------------------------------------------------------------------- /pkg/reset/nuker.go: -------------------------------------------------------------------------------- 1 | package reset 2 | 3 | import ( 4 | "github.com/rebuy-de/aws-nuke/cmd" 5 | "github.com/rebuy-de/aws-nuke/pkg/awsutil" 6 | "github.com/rebuy-de/aws-nuke/pkg/config" 7 | ) 8 | 9 | // Nuker interface requires methods that are necessary to set up and 10 | // execute a Nuke in an AWS Account. 11 | type Nuker interface { 12 | NewAccount(awsutil.Credentials) (*awsutil.Account, error) 13 | Load(string) (*config.Nuke, error) 14 | Run(*cmd.Nuke) error 15 | } 16 | 17 | // Nuke implements the NukeService interface using rebuy-de/aws-nuke 18 | // https://github.com/rebuy-de/aws-nuke 19 | type Nuke struct { 20 | } 21 | 22 | // NewAccount returns an aws-nuke Account that is created from the provided 23 | // aws-nuke Credentials. This will provide the account information needed for 24 | // aws-nuke to access an account to nuke. 25 | func (nuke Nuke) NewAccount(creds awsutil.Credentials) (*awsutil.Account, 26 | error) { 27 | return awsutil.NewAccount(creds, config.CustomEndpoints{}) 28 | } 29 | 30 | // Load returns an aws-nuke Nuke configuration with the provided configuration 31 | // file. This will provide the information needed to know what can be nuked by 32 | // aws-nuke. 33 | func (nuke Nuke) Load(configPath string) (*config.Nuke, error) { 34 | return config.Load(configPath) 35 | } 36 | 37 | // Run executes and returns the result of the aws-nuke nuke. 38 | func (nuke Nuke) Run(cmd *cmd.Nuke) error { 39 | return cmd.Run() 40 | } 41 | -------------------------------------------------------------------------------- /pkg/account/mocks/Manager.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import account "github.com/Optum/dce/pkg/account" 6 | import arn "github.com/Optum/dce/pkg/arn" 7 | import mock "github.com/stretchr/testify/mock" 8 | 9 | // Manager is an autogenerated mock type for the Manager type 10 | type Manager struct { 11 | mock.Mock 12 | } 13 | 14 | // DeletePrincipalAccess provides a mock function with given fields: _a0 15 | func (_m *Manager) DeletePrincipalAccess(_a0 *account.Account) error { 16 | ret := _m.Called(_a0) 17 | 18 | var r0 error 19 | if rf, ok := ret.Get(0).(func(*account.Account) error); ok { 20 | r0 = rf(_a0) 21 | } else { 22 | r0 = ret.Error(0) 23 | } 24 | 25 | return r0 26 | } 27 | 28 | // UpsertPrincipalAccess provides a mock function with given fields: _a0 29 | func (_m *Manager) UpsertPrincipalAccess(_a0 *account.Account) error { 30 | ret := _m.Called(_a0) 31 | 32 | var r0 error 33 | if rf, ok := ret.Get(0).(func(*account.Account) error); ok { 34 | r0 = rf(_a0) 35 | } else { 36 | r0 = ret.Error(0) 37 | } 38 | 39 | return r0 40 | } 41 | 42 | // ValidateAccess provides a mock function with given fields: role 43 | func (_m *Manager) ValidateAccess(role *arn.ARN) error { 44 | ret := _m.Called(role) 45 | 46 | var r0 error 47 | if rf, ok := ret.Get(0).(func(*arn.ARN) error); ok { 48 | r0 = rf(role) 49 | } else { 50 | r0 = ret.Error(0) 51 | } 52 | 53 | return r0 54 | } 55 | -------------------------------------------------------------------------------- /tools/awsnukedocgen/samples/nuke/resources/cloud9-environments.go.txt: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws" 5 | "github.com/aws/aws-sdk-go/aws/session" 6 | "github.com/aws/aws-sdk-go/service/cloud9" 7 | ) 8 | 9 | type Cloud9Environment struct { 10 | svc *cloud9.Cloud9 11 | environmentID *string 12 | } 13 | 14 | func init() { 15 | register("Cloud9Environment", ListCloud9Environments) 16 | } 17 | 18 | func ListCloud9Environments(sess *session.Session) ([]Resource, error) { 19 | svc := cloud9.New(sess) 20 | resources := []Resource{} 21 | 22 | params := &cloud9.ListEnvironmentsInput{ 23 | MaxResults: aws.Int64(25), 24 | } 25 | 26 | for { 27 | resp, err := svc.ListEnvironments(params) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | for _, environmentID := range resp.EnvironmentIds { 33 | resources = append(resources, &Cloud9Environment{ 34 | svc: svc, 35 | environmentID: environmentID, 36 | }) 37 | } 38 | 39 | if resp.NextToken == nil { 40 | break 41 | } 42 | 43 | params.NextToken = resp.NextToken 44 | } 45 | 46 | return resources, nil 47 | } 48 | 49 | func (f *Cloud9Environment) Remove() error { 50 | 51 | _, err := f.svc.DeleteEnvironment(&cloud9.DeleteEnvironmentInput{ 52 | EnvironmentId: f.environmentID, 53 | }) 54 | 55 | return err 56 | } 57 | 58 | func (f *Cloud9Environment) String() string { 59 | return *f.environmentID 60 | } 61 | -------------------------------------------------------------------------------- /tools/awsnukedocgen/samples/nuke/resources/neptune-instances.go.txt: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws" 5 | "github.com/aws/aws-sdk-go/aws/session" 6 | "github.com/aws/aws-sdk-go/service/neptune" 7 | ) 8 | 9 | type NeptuneInstance struct { 10 | svc *neptune.Neptune 11 | ID *string 12 | } 13 | 14 | func init() { 15 | register("NeptuneInstance", ListNeptuneInstances) 16 | } 17 | 18 | func ListNeptuneInstances(sess *session.Session) ([]Resource, error) { 19 | svc := neptune.New(sess) 20 | resources := []Resource{} 21 | 22 | params := &neptune.DescribeDBInstancesInput{ 23 | MaxRecords: aws.Int64(100), 24 | } 25 | 26 | for { 27 | output, err := svc.DescribeDBInstances(params) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | for _, dbInstance := range output.DBInstances { 33 | resources = append(resources, &NeptuneInstance{ 34 | svc: svc, 35 | ID: dbInstance.DBInstanceIdentifier, 36 | }) 37 | } 38 | 39 | if output.Marker == nil { 40 | break 41 | } 42 | 43 | params.Marker = output.Marker 44 | } 45 | 46 | return resources, nil 47 | } 48 | 49 | func (f *NeptuneInstance) Remove() error { 50 | 51 | _, err := f.svc.DeleteDBInstance(&neptune.DeleteDBInstanceInput{ 52 | DBInstanceIdentifier: f.ID, 53 | SkipFinalSnapshot: aws.Bool(true), 54 | }) 55 | 56 | return err 57 | } 58 | 59 | func (f *NeptuneInstance) String() string { 60 | return *f.ID 61 | } 62 | -------------------------------------------------------------------------------- /pkg/data/helpers_test.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/Optum/dce/pkg/account" 7 | "github.com/Optum/dce/pkg/arn" 8 | "github.com/aws/aws-sdk-go/service/dynamodb/expression" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func ptrString(s string) *string { 13 | ptrS := s 14 | return &ptrS 15 | } 16 | 17 | func ptrInt64(i int64) *int64 { 18 | ptrI := i 19 | return &ptrI 20 | } 21 | 22 | func TestHelpersBuildFilter(t *testing.T) { 23 | 24 | tests := []struct { 25 | name string 26 | query string 27 | i interface{} 28 | result expression.ConditionBuilder 29 | err error 30 | }{ 31 | { 32 | name: "buildfilter", 33 | i: &account.Account{ 34 | ID: ptrString("1"), 35 | }, 36 | result: expression.Name("Id").Equal(expression.Value("1")), 37 | }, 38 | { 39 | name: "multipleFilters", 40 | i: &account.Account{ 41 | ID: ptrString("1"), 42 | AdminRoleArn: arn.New("aws", "iam", "", "123456789012", "role/AdminRoleArn"), 43 | }, 44 | result: expression.And( 45 | expression.Name("Id").Equal(expression.Value("1")), 46 | expression.Name("AdminRoleArn").Equal(expression.Value("arn:aws:iam::123456789012:role/AdminRoleArn")), 47 | ), 48 | }, 49 | } 50 | 51 | for _, tt := range tests { 52 | t.Run(tt.name, func(t *testing.T) { 53 | _, o := getFiltersFromStruct(tt.i, nil) 54 | assert.Equal(t, &tt.result, o) 55 | }) 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /tools/awsnukedocgen/samples/nuke/resources/emr-securityconfigurations.go.txt: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws/session" 5 | "github.com/aws/aws-sdk-go/service/emr" 6 | ) 7 | 8 | type EMRSecurityConfiguration struct { 9 | svc *emr.EMR 10 | name *string 11 | } 12 | 13 | func init() { 14 | register("EMRSecurityConfiguration", ListEMRSecurityConfiguration) 15 | } 16 | 17 | func ListEMRSecurityConfiguration(sess *session.Session) ([]Resource, error) { 18 | svc := emr.New(sess) 19 | resources := []Resource{} 20 | 21 | params := &emr.ListSecurityConfigurationsInput{} 22 | 23 | for { 24 | resp, err := svc.ListSecurityConfigurations(params) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | for _, securityConfiguration := range resp.SecurityConfigurations { 30 | resources = append(resources, &EMRSecurityConfiguration{ 31 | svc: svc, 32 | name: securityConfiguration.Name, 33 | }) 34 | } 35 | 36 | if resp.Marker == nil { 37 | break 38 | } 39 | 40 | params.Marker = resp.Marker 41 | } 42 | 43 | return resources, nil 44 | } 45 | 46 | func (f *EMRSecurityConfiguration) Remove() error { 47 | 48 | //Call names are inconsistent in the SDK 49 | _, err := f.svc.DeleteSecurityConfiguration(&emr.DeleteSecurityConfigurationInput{ 50 | Name: f.name, 51 | }) 52 | 53 | return err 54 | } 55 | 56 | func (f *EMRSecurityConfiguration) String() string { 57 | return *f.name 58 | } 59 | -------------------------------------------------------------------------------- /tools/awsnukedocgen/samples/nuke/resources/opsworks-instances.go.txt: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws/session" 5 | "github.com/aws/aws-sdk-go/service/opsworks" 6 | ) 7 | 8 | type OpsWorksInstance struct { 9 | svc *opsworks.OpsWorks 10 | ID *string 11 | } 12 | 13 | func init() { 14 | register("OpsWorksInstance", ListOpsWorksInstances) 15 | } 16 | 17 | func ListOpsWorksInstances(sess *session.Session) ([]Resource, error) { 18 | svc := opsworks.New(sess) 19 | resources := []Resource{} 20 | 21 | stackParams := &opsworks.DescribeStacksInput{} 22 | 23 | resp, err := svc.DescribeStacks(stackParams) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | instanceParams := &opsworks.DescribeInstancesInput{} 29 | for _, stack := range resp.Stacks { 30 | instanceParams.StackId = stack.StackId 31 | output, err := svc.DescribeInstances(instanceParams) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | for _, instance := range output.Instances { 37 | resources = append(resources, &OpsWorksInstance{ 38 | svc: svc, 39 | ID: instance.InstanceId, 40 | }) 41 | } 42 | } 43 | 44 | return resources, nil 45 | } 46 | 47 | func (f *OpsWorksInstance) Remove() error { 48 | 49 | _, err := f.svc.DeleteInstance(&opsworks.DeleteInstanceInput{ 50 | InstanceId: f.ID, 51 | }) 52 | 53 | return err 54 | } 55 | 56 | func (f *OpsWorksInstance) String() string { 57 | return *f.ID 58 | } 59 | -------------------------------------------------------------------------------- /tools/awsnukedocgen/samples/nuke/resources/autoscaling-launchconfigurations.go.txt: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws/session" 5 | "github.com/aws/aws-sdk-go/service/autoscaling" 6 | ) 7 | 8 | func init() { 9 | register("LaunchConfiguration", ListLaunchConfigurations) 10 | } 11 | 12 | func ListLaunchConfigurations(s *session.Session) ([]Resource, error) { 13 | svc := autoscaling.New(s) 14 | 15 | params := &autoscaling.DescribeLaunchConfigurationsInput{} 16 | resp, err := svc.DescribeLaunchConfigurations(params) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | resources := make([]Resource, 0) 22 | for _, launchconfig := range resp.LaunchConfigurations { 23 | resources = append(resources, &LaunchConfiguration{ 24 | svc: svc, 25 | name: launchconfig.LaunchConfigurationName, 26 | }) 27 | } 28 | return resources, nil 29 | } 30 | 31 | type LaunchConfiguration struct { 32 | svc *autoscaling.AutoScaling 33 | name *string 34 | } 35 | 36 | func (launchconfiguration *LaunchConfiguration) Remove() error { 37 | params := &autoscaling.DeleteLaunchConfigurationInput{ 38 | LaunchConfigurationName: launchconfiguration.name, 39 | } 40 | 41 | _, err := launchconfiguration.svc.DeleteLaunchConfiguration(params) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | return nil 47 | } 48 | 49 | func (launchconfiguration *LaunchConfiguration) String() string { 50 | return *launchconfiguration.name 51 | } 52 | -------------------------------------------------------------------------------- /tools/awsnukedocgen/samples/nuke/resources/kinesis-streams.go.txt: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws" 5 | "github.com/aws/aws-sdk-go/aws/session" 6 | "github.com/aws/aws-sdk-go/service/kinesis" 7 | ) 8 | 9 | type KinesisStream struct { 10 | svc *kinesis.Kinesis 11 | streamName *string 12 | } 13 | 14 | func init() { 15 | register("KinesisStream", ListKinesisStreams) 16 | } 17 | 18 | func ListKinesisStreams(sess *session.Session) ([]Resource, error) { 19 | svc := kinesis.New(sess) 20 | resources := []Resource{} 21 | var lastStreamName *string 22 | params := &kinesis.ListStreamsInput{ 23 | Limit: aws.Int64(25), 24 | } 25 | 26 | for { 27 | output, err := svc.ListStreams(params) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | for _, streamName := range output.StreamNames { 33 | resources = append(resources, &KinesisStream{ 34 | svc: svc, 35 | streamName: streamName, 36 | }) 37 | lastStreamName = streamName 38 | } 39 | 40 | if *output.HasMoreStreams == false { 41 | break 42 | } 43 | 44 | params.ExclusiveStartStreamName = lastStreamName 45 | } 46 | 47 | return resources, nil 48 | } 49 | 50 | func (f *KinesisStream) Remove() error { 51 | 52 | _, err := f.svc.DeleteStream(&kinesis.DeleteStreamInput{ 53 | StreamName: f.streamName, 54 | }) 55 | 56 | return err 57 | } 58 | 59 | func (f *KinesisStream) String() string { 60 | return *f.streamName 61 | } 62 | -------------------------------------------------------------------------------- /tools/awsnukedocgen/samples/nuke/resources/msk-cluster.go.txt: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws/session" 5 | "github.com/aws/aws-sdk-go/service/kafka" 6 | "github.com/rebuy-de/aws-nuke/pkg/types" 7 | ) 8 | 9 | type MSKCluster struct { 10 | svc *kafka.Kafka 11 | arn string 12 | name string 13 | } 14 | 15 | func init() { 16 | register("MSKCluster", ListMSKCluster) 17 | } 18 | 19 | func ListMSKCluster(sess *session.Session) ([]Resource, error) { 20 | svc := kafka.New(sess) 21 | params := &kafka.ListClustersInput{} 22 | resp, err := svc.ListClusters(params) 23 | 24 | if err != nil { 25 | return nil, err 26 | } 27 | resources := make([]Resource, 0) 28 | for _, cluster := range resp.ClusterInfoList { 29 | resources = append(resources, &MSKCluster{ 30 | svc: svc, 31 | arn: *cluster.ClusterArn, 32 | name: *cluster.ClusterName, 33 | }) 34 | 35 | } 36 | return resources, nil 37 | } 38 | 39 | func (m *MSKCluster) Remove() error { 40 | params := &kafka.DeleteClusterInput{ 41 | ClusterArn: &m.arn, 42 | } 43 | 44 | _, err := m.svc.DeleteCluster(params) 45 | if err != nil { 46 | return err 47 | } 48 | return nil 49 | } 50 | 51 | func (m *MSKCluster) String() string { 52 | return m.arn 53 | } 54 | 55 | func (m *MSKCluster) Properties() types.Properties { 56 | properties := types.NewProperties() 57 | properties.Set("ARN", m.arn) 58 | properties.Set("Name", m.name) 59 | 60 | return properties 61 | } 62 | -------------------------------------------------------------------------------- /pkg/account/validate_test.go: -------------------------------------------------------------------------------- 1 | package account_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/Optum/dce/pkg/account" 9 | "github.com/Optum/dce/pkg/arn" 10 | "github.com/Optum/dce/pkg/errors" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestValidate(t *testing.T) { 15 | now := time.Now().Unix() 16 | 17 | tests := []struct { 18 | name string 19 | expErr error 20 | account account.Account 21 | }{ 22 | { 23 | name: "should validate", 24 | account: account.Account{ 25 | ID: ptrString("123456789012"), 26 | Status: account.StatusReady.StatusPtr(), 27 | AdminRoleArn: arn.New("aws", "iam", "", "123456789012", "role/AdminRoleArn"), 28 | CreatedOn: &now, 29 | LastModifiedOn: &now, 30 | }, 31 | }, 32 | { 33 | name: "should not validate no admin role", 34 | account: account.Account{ 35 | ID: ptrString("123456789012"), 36 | Status: account.StatusLeased.StatusPtr(), 37 | CreatedOn: &now, 38 | LastModifiedOn: &now, 39 | }, 40 | expErr: errors.NewValidation("account", fmt.Errorf("adminRoleArn: must be a string.")), //nolint golint 41 | }, 42 | } 43 | 44 | for _, tt := range tests { 45 | t.Run(tt.name, func(t *testing.T) { 46 | 47 | err := tt.account.Validate() 48 | assert.True(t, errors.Is(err, tt.expErr), "actual error %q doesn't match expected error %q", err, tt.expErr) 49 | 50 | }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /pkg/account/mocks/Reader.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import account "github.com/Optum/dce/pkg/account" 6 | import mock "github.com/stretchr/testify/mock" 7 | 8 | // Reader is an autogenerated mock type for the Reader type 9 | type Reader struct { 10 | mock.Mock 11 | } 12 | 13 | // Get provides a mock function with given fields: ID 14 | func (_m *Reader) Get(ID string) (*account.Account, error) { 15 | ret := _m.Called(ID) 16 | 17 | var r0 *account.Account 18 | if rf, ok := ret.Get(0).(func(string) *account.Account); ok { 19 | r0 = rf(ID) 20 | } else { 21 | if ret.Get(0) != nil { 22 | r0 = ret.Get(0).(*account.Account) 23 | } 24 | } 25 | 26 | var r1 error 27 | if rf, ok := ret.Get(1).(func(string) error); ok { 28 | r1 = rf(ID) 29 | } else { 30 | r1 = ret.Error(1) 31 | } 32 | 33 | return r0, r1 34 | } 35 | 36 | // List provides a mock function with given fields: query 37 | func (_m *Reader) List(query *account.Account) (*account.Accounts, error) { 38 | ret := _m.Called(query) 39 | 40 | var r0 *account.Accounts 41 | if rf, ok := ret.Get(0).(func(*account.Account) *account.Accounts); ok { 42 | r0 = rf(query) 43 | } else { 44 | if ret.Get(0) != nil { 45 | r0 = ret.Get(0).(*account.Accounts) 46 | } 47 | } 48 | 49 | var r1 error 50 | if rf, ok := ret.Get(1).(func(*account.Account) error); ok { 51 | r1 = rf(query) 52 | } else { 53 | r1 = ret.Error(1) 54 | } 55 | 56 | return r0, r1 57 | } 58 | -------------------------------------------------------------------------------- /pkg/accountmanager/accountmanageriface/mocks/Servicer.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import account "github.com/Optum/dce/pkg/account" 6 | 7 | import arn "github.com/Optum/dce/pkg/arn" 8 | import mock "github.com/stretchr/testify/mock" 9 | 10 | // Servicer is an autogenerated mock type for the Servicer type 11 | type Servicer struct { 12 | mock.Mock 13 | } 14 | 15 | // DeletePrincipalAccess provides a mock function with given fields: _a0 16 | func (_m *Servicer) DeletePrincipalAccess(_a0 *account.Account) error { 17 | ret := _m.Called(_a0) 18 | 19 | var r0 error 20 | if rf, ok := ret.Get(0).(func(*account.Account) error); ok { 21 | r0 = rf(_a0) 22 | } else { 23 | r0 = ret.Error(0) 24 | } 25 | 26 | return r0 27 | } 28 | 29 | // UpsertPrincipalAccess provides a mock function with given fields: _a0 30 | func (_m *Servicer) UpsertPrincipalAccess(_a0 *account.Account) error { 31 | ret := _m.Called(_a0) 32 | 33 | var r0 error 34 | if rf, ok := ret.Get(0).(func(*account.Account) error); ok { 35 | r0 = rf(_a0) 36 | } else { 37 | r0 = ret.Error(0) 38 | } 39 | 40 | return r0 41 | } 42 | 43 | // ValidateAccess provides a mock function with given fields: role 44 | func (_m *Servicer) ValidateAccess(role *arn.ARN) error { 45 | ret := _m.Called(role) 46 | 47 | var r0 error 48 | if rf, ok := ret.Get(0).(func(*arn.ARN) error); ok { 49 | r0 = rf(role) 50 | } else { 51 | r0 = ret.Error(0) 52 | } 53 | 54 | return r0 55 | } 56 | -------------------------------------------------------------------------------- /tools/awsnukedocgen/samples/nuke/resources/sns-subscriptions.go.txt: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/aws/aws-sdk-go/aws/session" 7 | "github.com/aws/aws-sdk-go/service/sns" 8 | ) 9 | 10 | func init() { 11 | register("SNSSubscription", ListSNSSubscriptions) 12 | } 13 | 14 | func ListSNSSubscriptions(sess *session.Session) ([]Resource, error) { 15 | svc := sns.New(sess) 16 | 17 | params := &sns.ListSubscriptionsInput{} 18 | resources := make([]Resource, 0) 19 | 20 | for { 21 | resp, err := svc.ListSubscriptions(params) 22 | if err != nil { 23 | return nil, err 24 | } 25 | for _, subscription := range resp.Subscriptions { 26 | if *subscription.SubscriptionArn != "PendingConfirmation" { 27 | resources = append(resources, &SNSSubscription{ 28 | svc: svc, 29 | id: subscription.SubscriptionArn, 30 | name: subscription.Owner, 31 | }) 32 | } 33 | 34 | } 35 | 36 | if resp.NextToken == nil { 37 | break 38 | } 39 | 40 | params.NextToken = resp.NextToken 41 | } 42 | 43 | return resources, nil 44 | } 45 | 46 | type SNSSubscription struct { 47 | svc *sns.SNS 48 | id *string 49 | name *string 50 | } 51 | 52 | func (subs *SNSSubscription) Remove() error { 53 | _, err := subs.svc.Unsubscribe(&sns.UnsubscribeInput{ 54 | SubscriptionArn: subs.id, 55 | }) 56 | return err 57 | } 58 | 59 | func (subs *SNSSubscription) String() string { 60 | return fmt.Sprintf("Owner: %s ARN: %s", *subs.name, *subs.id) 61 | } 62 | -------------------------------------------------------------------------------- /tools/awsnukedocgen/samples/nuke/resources/route53-hosted-zones.go.txt: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/aws/aws-sdk-go/aws/session" 7 | "github.com/aws/aws-sdk-go/service/route53" 8 | "github.com/rebuy-de/aws-nuke/pkg/types" 9 | ) 10 | 11 | func init() { 12 | register("Route53HostedZone", ListRoute53HostedZones) 13 | } 14 | 15 | func ListRoute53HostedZones(sess *session.Session) ([]Resource, error) { 16 | svc := route53.New(sess) 17 | 18 | params := &route53.ListHostedZonesInput{} 19 | resp, err := svc.ListHostedZones(params) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | resources := make([]Resource, 0) 25 | for _, hz := range resp.HostedZones { 26 | resources = append(resources, &Route53HostedZone{ 27 | svc: svc, 28 | id: hz.Id, 29 | name: hz.Name, 30 | }) 31 | } 32 | return resources, nil 33 | } 34 | 35 | type Route53HostedZone struct { 36 | svc *route53.Route53 37 | id *string 38 | name *string 39 | } 40 | 41 | func (hz *Route53HostedZone) Remove() error { 42 | params := &route53.DeleteHostedZoneInput{ 43 | Id: hz.id, 44 | } 45 | 46 | _, err := hz.svc.DeleteHostedZone(params) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | return nil 52 | } 53 | 54 | func (hz *Route53HostedZone) Properties() types.Properties { 55 | return types.NewProperties(). 56 | Set("Name", hz.name) 57 | } 58 | 59 | func (hz *Route53HostedZone) String() string { 60 | return fmt.Sprintf("%s (%s)", *hz.id, *hz.name) 61 | } 62 | -------------------------------------------------------------------------------- /pkg/lease/validate_test.go: -------------------------------------------------------------------------------- 1 | package lease_test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/Optum/dce/pkg/lease" 6 | "testing" 7 | "time" 8 | 9 | "github.com/Optum/dce/pkg/errors" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestValidate(t *testing.T) { 14 | now := time.Now().Unix() 15 | 16 | tests := []struct { 17 | name string 18 | expErr error 19 | lease lease.Lease 20 | }{ 21 | { 22 | name: "should validate", 23 | lease: lease.Lease{ 24 | ID: ptrString("d892f19d-3204-4674-aa78-bd12de5ff226"), 25 | AccountID: ptrString("123456789012"), 26 | PrincipalID: ptrString("user1"), 27 | Status: lease.StatusActive.StatusPtr(), 28 | CreatedOn: &now, 29 | LastModifiedOn: &now, 30 | }, 31 | }, 32 | { 33 | name: "should not validate no status", 34 | lease: lease.Lease{ 35 | ID: ptrString("d892f19d-3204-4674-aa78-bd12de5ff226"), 36 | AccountID: ptrString("123456789012"), 37 | PrincipalID: ptrString("user1"), 38 | CreatedOn: &now, 39 | LastModifiedOn: &now, 40 | }, 41 | expErr: errors.NewValidation("lease", fmt.Errorf("leaseStatus: must be a valid lease status.")), //nolint golint 42 | }, 43 | } 44 | 45 | for _, tt := range tests { 46 | t.Run(tt.name, func(t *testing.T) { 47 | 48 | err := tt.lease.Validate() 49 | assert.True(t, errors.Is(err, tt.expErr), "actual error %q doesn't match expected error %q", err, tt.expErr) 50 | 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /cmd/lambda/credentials_web_page/views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 |

DCE CLI Credentials

25 | 26 | 27 | 28 |
29 | 30 | 31 | -------------------------------------------------------------------------------- /tools/awsnukedocgen/samples/nuke/resources/redshift-clusters.go.txt: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws" 5 | "github.com/aws/aws-sdk-go/aws/session" 6 | "github.com/aws/aws-sdk-go/service/redshift" 7 | ) 8 | 9 | type RedshiftCluster struct { 10 | svc *redshift.Redshift 11 | clusterIdentifier *string 12 | } 13 | 14 | func init() { 15 | register("RedshiftCluster", ListRedshiftClusters) 16 | } 17 | 18 | func ListRedshiftClusters(sess *session.Session) ([]Resource, error) { 19 | svc := redshift.New(sess) 20 | resources := []Resource{} 21 | 22 | params := &redshift.DescribeClustersInput{ 23 | MaxRecords: aws.Int64(100), 24 | } 25 | 26 | for { 27 | output, err := svc.DescribeClusters(params) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | for _, cluster := range output.Clusters { 33 | resources = append(resources, &RedshiftCluster{ 34 | svc: svc, 35 | clusterIdentifier: cluster.ClusterIdentifier, 36 | }) 37 | } 38 | 39 | if output.Marker == nil { 40 | break 41 | } 42 | 43 | params.Marker = output.Marker 44 | } 45 | 46 | return resources, nil 47 | } 48 | 49 | func (f *RedshiftCluster) Remove() error { 50 | 51 | _, err := f.svc.DeleteCluster(&redshift.DeleteClusterInput{ 52 | ClusterIdentifier: f.clusterIdentifier, 53 | SkipFinalClusterSnapshot: aws.Bool(true), 54 | }) 55 | 56 | return err 57 | } 58 | 59 | func (f *RedshiftCluster) String() string { 60 | return *f.clusterIdentifier 61 | } 62 | -------------------------------------------------------------------------------- /pkg/event/eventbus.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/Optum/dce/pkg/errors" 7 | "github.com/aws/aws-sdk-go/aws" 8 | "github.com/aws/aws-sdk-go/service/cloudwatchevents" 9 | "github.com/aws/aws-sdk-go/service/cloudwatchevents/cloudwatcheventsiface" 10 | ) 11 | 12 | const ( 13 | cweSource string = "dce" 14 | ) 15 | 16 | // CloudWatchEvent is for publishing events to Event Bus 17 | type CloudWatchEvent struct { 18 | cw cloudwatcheventsiface.CloudWatchEventsAPI 19 | detailType *string 20 | } 21 | 22 | // Publish an event to the topic 23 | func (c *CloudWatchEvent) Publish(i interface{}) error { 24 | bodyJSON, err := json.Marshal(i) 25 | if err != nil { 26 | return errors.NewInternalServer("unable to marshal response", err) 27 | } 28 | 29 | // Send the message 30 | _, err = c.cw.PutEvents(&cloudwatchevents.PutEventsInput{ 31 | Entries: []*cloudwatchevents.PutEventsRequestEntry{ 32 | { 33 | Detail: aws.String(string(bodyJSON)), 34 | DetailType: c.detailType, 35 | Source: aws.String(cweSource), 36 | }, 37 | }, 38 | }) 39 | if err != nil { 40 | return errors.NewInternalServer("failed to publish message to CloudWatch Event Bus", err) 41 | } 42 | return nil 43 | } 44 | 45 | // NewCloudWatchEvent creates a new AWS Eventing Bus 46 | func NewCloudWatchEvent(cw cloudwatcheventsiface.CloudWatchEventsAPI, detailType string) (*CloudWatchEvent, error) { 47 | 48 | return &CloudWatchEvent{ 49 | cw: cw, 50 | detailType: &detailType, 51 | }, nil 52 | } 53 | -------------------------------------------------------------------------------- /pkg/budget/budget_test.go: -------------------------------------------------------------------------------- 1 | package budget 2 | 3 | import ( 4 | "github.com/Optum/dce/pkg/awsiface/mocks" 5 | "testing" 6 | "time" 7 | 8 | "github.com/aws/aws-sdk-go/aws" 9 | "github.com/aws/aws-sdk-go/service/costexplorer" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestCalculateTotalSpend(t *testing.T) { 14 | // Mock the CostExplorer SDK 15 | costExplorer := &mocks.CostExplorerAPI{} 16 | costExplorer.On("GetCostAndUsage", &costexplorer.GetCostAndUsageInput{ 17 | Metrics: []*string{aws.String("UnblendedCost")}, 18 | Granularity: aws.String("DAILY"), 19 | TimePeriod: &costexplorer.DateInterval{ 20 | Start: aws.String("1970-01-01"), 21 | End: aws.String("1970-01-02"), 22 | }, 23 | }).Return(&costexplorer.GetCostAndUsageOutput{ 24 | ResultsByTime: []*costexplorer.ResultByTime{ 25 | { 26 | Total: map[string]*costexplorer.MetricValue{ 27 | "UnblendedCost": { 28 | Amount: aws.String("100"), 29 | Unit: aws.String("USD"), 30 | }, 31 | }, 32 | }, 33 | { 34 | Total: map[string]*costexplorer.MetricValue{ 35 | "UnblendedCost": { 36 | Amount: aws.String("50"), 37 | Unit: aws.String("USD"), 38 | }, 39 | }, 40 | }, 41 | }, 42 | }, nil) 43 | 44 | budgetSvc := AWSBudgetService{ 45 | CostExplorer: costExplorer, 46 | } 47 | cost, err := budgetSvc.CalculateTotalSpend( 48 | time.Unix(0, 0), 49 | time.Unix(0, 0).Add(time.Hour*24), 50 | ) 51 | assert.Nil(t, err, "There should be no errors") 52 | assert.Equal(t, cost, float64(150)) 53 | } 54 | -------------------------------------------------------------------------------- /pkg/usage/mocks/Reader.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | import usage "github.com/Optum/dce/pkg/usage" 7 | 8 | // Reader is an autogenerated mock type for the Reader type 9 | type Reader struct { 10 | mock.Mock 11 | } 12 | 13 | // Get provides a mock function with given fields: startDate, principalID 14 | func (_m *Reader) Get(startDate int64, principalID string) (*usage.Usage, error) { 15 | ret := _m.Called(startDate, principalID) 16 | 17 | var r0 *usage.Usage 18 | if rf, ok := ret.Get(0).(func(int64, string) *usage.Usage); ok { 19 | r0 = rf(startDate, principalID) 20 | } else { 21 | if ret.Get(0) != nil { 22 | r0 = ret.Get(0).(*usage.Usage) 23 | } 24 | } 25 | 26 | var r1 error 27 | if rf, ok := ret.Get(1).(func(int64, string) error); ok { 28 | r1 = rf(startDate, principalID) 29 | } else { 30 | r1 = ret.Error(1) 31 | } 32 | 33 | return r0, r1 34 | } 35 | 36 | // List provides a mock function with given fields: query 37 | func (_m *Reader) List(query *usage.Usage) (*usage.Usages, error) { 38 | ret := _m.Called(query) 39 | 40 | var r0 *usage.Usages 41 | if rf, ok := ret.Get(0).(func(*usage.Usage) *usage.Usages); ok { 42 | r0 = rf(query) 43 | } else { 44 | if ret.Get(0) != nil { 45 | r0 = ret.Get(0).(*usage.Usages) 46 | } 47 | } 48 | 49 | var r1 error 50 | if rf, ok := ret.Get(1).(func(*usage.Usage) error); ok { 51 | r1 = rf(query) 52 | } else { 53 | r1 = ret.Error(1) 54 | } 55 | 56 | return r0, r1 57 | } 58 | -------------------------------------------------------------------------------- /modules/lambda/lambda.tf: -------------------------------------------------------------------------------- 1 | resource "aws_lambda_function" "fn" { 2 | function_name = var.name 3 | description = var.description 4 | runtime = "go1.x" 5 | handler = var.handler 6 | role = aws_iam_role.lambda_execution.arn 7 | timeout = var.timeout 8 | 9 | # Stub an application deployment 10 | # (deployments will be managed outside terraform) 11 | filename = data.archive_file.lambda_code_stub.output_path 12 | 13 | lifecycle { 14 | # Filename will change, as new application deployments are pushed. 15 | # Prevent terraform from reverting to old application deployments 16 | # We're not using terraform to manage application deployments 17 | ignore_changes = [filename] 18 | } 19 | 20 | environment { 21 | variables = var.environment 22 | } 23 | 24 | dynamic "dead_letter_config" { 25 | for_each = aws_sqs_queue.lambda_dlq.*.arn 26 | content { 27 | target_arn = dead_letter_config.value 28 | } 29 | } 30 | 31 | tags = var.global_tags 32 | } 33 | 34 | 35 | # Lambda code deployments are managed outside of Terraform, 36 | # by our Jenkins pipeline. 37 | # However, Lambda TF resource require a code file to initialize. 38 | # We'll create an empty "stub" file to initialize the lambda function, 39 | # and then publish the code afterwards from jenkins. 40 | data "archive_file" "lambda_code_stub" { 41 | type = "zip" 42 | output_path = "${path.module}/lambda_stub.zip" 43 | 44 | source { 45 | filename = "stub_file" 46 | content = "STUB CONTENT" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /modules/alarms_sns.tf: -------------------------------------------------------------------------------- 1 | # Make a topic 2 | resource "aws_sns_topic" "alarms_topic" { 3 | name = "alarms-${var.namespace}" 4 | } 5 | 6 | resource "aws_sns_topic_policy" "alarms_policy" { 7 | arn = aws_sns_topic.alarms_topic.arn 8 | policy = < 2 | 3 | 4 | 5 | 6 | Swagger UI 7 | 8 | 14 | 20 | 38 | 39 | 40 | 41 |
42 | 43 | 44 | 45 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /tools/awsnukedocgen/samples/nuke/resources/lambda-functions.go.txt: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws/session" 5 | "github.com/aws/aws-sdk-go/service/lambda" 6 | "github.com/rebuy-de/aws-nuke/pkg/types" 7 | ) 8 | 9 | type LambdaFunction struct { 10 | svc *lambda.Lambda 11 | functionName *string 12 | tags map[string]*string 13 | } 14 | 15 | func init() { 16 | register("LambdaFunction", ListLambdaFunctions) 17 | } 18 | 19 | func ListLambdaFunctions(sess *session.Session) ([]Resource, error) { 20 | svc := lambda.New(sess) 21 | 22 | params := &lambda.ListFunctionsInput{} 23 | resp, err := svc.ListFunctions(params) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | resources := make([]Resource, 0) 29 | for _, function := range resp.Functions { 30 | tags, err := svc.ListTags(&lambda.ListTagsInput{ 31 | Resource: function.FunctionArn, 32 | }) 33 | 34 | if err != nil { 35 | continue 36 | } 37 | 38 | resources = append(resources, &LambdaFunction{ 39 | svc: svc, 40 | functionName: function.FunctionName, 41 | tags: tags.Tags, 42 | }) 43 | } 44 | 45 | return resources, nil 46 | } 47 | 48 | func (f *LambdaFunction) Properties() types.Properties { 49 | properties := types.NewProperties() 50 | properties.Set("Name", f.functionName) 51 | 52 | for key, val := range f.tags { 53 | properties.SetTag(&key, val) 54 | } 55 | 56 | return properties 57 | } 58 | 59 | func (f *LambdaFunction) Remove() error { 60 | 61 | _, err := f.svc.DeleteFunction(&lambda.DeleteFunctionInput{ 62 | FunctionName: f.functionName, 63 | }) 64 | 65 | return err 66 | } 67 | 68 | func (f *LambdaFunction) String() string { 69 | return *f.functionName 70 | } 71 | -------------------------------------------------------------------------------- /pkg/api/response/lease.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "github.com/Optum/dce/pkg/db" 5 | ) 6 | 7 | // CreateLeaseResponse creates an Lease Response based 8 | // on the provided Lease 9 | func CreateLeaseResponse(lease *db.Lease) *LeaseResponse { 10 | response := LeaseResponse(*lease) 11 | return &response 12 | } 13 | 14 | // LeaseResponse is the structured JSON Response for an Lease 15 | // to be returned for APIs 16 | // { 17 | // "accountId": "123", 18 | // "principalId": "user", 19 | // "leaseStatus": "Active", 20 | // "createdOn": 56789, 21 | // "lastModifiedOn": 56789, 22 | // "budgetAmount": 300, 23 | // "BudgetCurrency": "USD", 24 | // "BudgetNotificationEmails": ["usermsid@test.com", "managersmsid@test.com"] 25 | // } 26 | type LeaseResponse struct { 27 | AccountID string `json:"accountId"` 28 | PrincipalID string `json:"principalId"` 29 | ID string `json:"id"` 30 | LeaseStatus db.LeaseStatus `json:"leaseStatus"` 31 | LeaseStatusReason db.LeaseStatusReason `json:"leaseStatusReason"` 32 | CreatedOn int64 `json:"createdOn"` 33 | LastModifiedOn int64 `json:"lastModifiedOn"` 34 | BudgetAmount float64 `json:"budgetAmount"` 35 | BudgetCurrency string `json:"budgetCurrency"` 36 | BudgetNotificationEmails []string `json:"budgetNotificationEmails"` 37 | LeaseStatusModifiedOn int64 `json:"leaseStatusModifiedOn"` 38 | ExpiresOn int64 `json:"expiresOn"` 39 | Metadata map[string]interface{} `json:"metadata"` 40 | } 41 | -------------------------------------------------------------------------------- /pkg/event/sns.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/Optum/dce/pkg/errors" 8 | "github.com/aws/aws-sdk-go/aws" 9 | "github.com/aws/aws-sdk-go/aws/arn" 10 | "github.com/aws/aws-sdk-go/service/sns" 11 | "github.com/aws/aws-sdk-go/service/sns/snsiface" 12 | ) 13 | 14 | // SnsEvent is for publishing events to SQS 15 | type SnsEvent struct { 16 | sns snsiface.SNSAPI 17 | topicArn arn.ARN 18 | } 19 | 20 | // Publish an event to the topic 21 | func (s *SnsEvent) Publish(i interface{}) error { 22 | bodyJSON, err := json.Marshal(i) 23 | if err != nil { 24 | return errors.NewInternalServer("unable to marshal response", err) 25 | } 26 | 27 | // Wrap the body in a SNS message object 28 | message, err := json.Marshal(map[string]string{ 29 | "default": string(bodyJSON), 30 | "Body": string(bodyJSON), 31 | }) 32 | if err != nil { 33 | return errors.NewInternalServer("failed to prepare SNS body JSON", err) 34 | } 35 | 36 | // Send the message 37 | _, err = s.sns.Publish(&sns.PublishInput{ 38 | Message: aws.String(string(message)), 39 | TopicArn: aws.String(s.topicArn.String()), 40 | MessageStructure: aws.String("json"), 41 | }) 42 | if err != nil { 43 | return errors.NewInternalServer("failed to publish message to SNS topic", err) 44 | } 45 | return nil 46 | } 47 | 48 | // NewSnsEvent creates a new SNS eventing struct 49 | func NewSnsEvent(sns snsiface.SNSAPI, a string) (*SnsEvent, error) { 50 | 51 | snsArn, err := arn.Parse(a) 52 | if err != nil { 53 | return nil, errors.NewInternalServer( 54 | fmt.Sprintf("unable to parse arn %q", a), 55 | err, 56 | ) 57 | } 58 | return &SnsEvent{ 59 | sns: sns, 60 | topicArn: snsArn, 61 | }, nil 62 | } 63 | -------------------------------------------------------------------------------- /pkg/rolemanager/mocks/PolicyManager.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | awsiface "github.com/Optum/dce/pkg/awsiface" 7 | mock "github.com/stretchr/testify/mock" 8 | 9 | rolemanager "github.com/Optum/dce/pkg/rolemanager" 10 | ) 11 | 12 | // PolicyManager is an autogenerated mock type for the PolicyManager type 13 | type PolicyManager struct { 14 | mock.Mock 15 | } 16 | 17 | // DeletePolicyVersion provides a mock function with given fields: arn, versionID 18 | func (_m *PolicyManager) DeletePolicyVersion(arn string, versionID string) error { 19 | ret := _m.Called(arn, versionID) 20 | 21 | var r0 error 22 | if rf, ok := ret.Get(0).(func(string, string) error); ok { 23 | r0 = rf(arn, versionID) 24 | } else { 25 | r0 = ret.Error(0) 26 | } 27 | 28 | return r0 29 | } 30 | 31 | // MergePolicy provides a mock function with given fields: input 32 | func (_m *PolicyManager) MergePolicy(input *rolemanager.MergePolicyInput) error { 33 | ret := _m.Called(input) 34 | 35 | var r0 error 36 | if rf, ok := ret.Get(0).(func(*rolemanager.MergePolicyInput) error); ok { 37 | r0 = rf(input) 38 | } else { 39 | r0 = ret.Error(0) 40 | } 41 | 42 | return r0 43 | } 44 | 45 | // PrunePolicyVersions provides a mock function with given fields: arn 46 | func (_m *PolicyManager) PrunePolicyVersions(arn string) error { 47 | ret := _m.Called(arn) 48 | 49 | var r0 error 50 | if rf, ok := ret.Get(0).(func(string) error); ok { 51 | r0 = rf(arn) 52 | } else { 53 | r0 = ret.Error(0) 54 | } 55 | 56 | return r0 57 | } 58 | 59 | // SetIAMClient provides a mock function with given fields: iamClient 60 | func (_m *PolicyManager) SetIAMClient(iamClient awsiface.IAM) { 61 | _m.Called(iamClient) 62 | } 63 | -------------------------------------------------------------------------------- /tools/awsnukedocgen/samples/nuke/resources/iam-virtual-mfa-devices.go.txt: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/aws/aws-sdk-go/aws/session" 8 | "github.com/aws/aws-sdk-go/service/iam" 9 | ) 10 | 11 | type IAMVirtualMFADevice struct { 12 | svc *iam.IAM 13 | user *iam.User 14 | serialNumber string 15 | } 16 | 17 | func init() { 18 | register("IAMVirtualMFADevice", ListIAMVirtualMFADevices) 19 | } 20 | 21 | func ListIAMVirtualMFADevices(sess *session.Session) ([]Resource, error) { 22 | svc := iam.New(sess) 23 | 24 | resp, err := svc.ListVirtualMFADevices(&iam.ListVirtualMFADevicesInput{}) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | resources := make([]Resource, 0) 30 | for _, out := range resp.VirtualMFADevices { 31 | resources = append(resources, &IAMVirtualMFADevice{ 32 | svc: svc, 33 | user: out.User, 34 | serialNumber: *out.SerialNumber, 35 | }) 36 | } 37 | 38 | return resources, nil 39 | } 40 | 41 | func (v *IAMVirtualMFADevice) Filter() error { 42 | if strings.HasSuffix(v.serialNumber, "/root-account-mfa-device") { 43 | return fmt.Errorf("Cannot delete root MFA device") 44 | } 45 | return nil 46 | } 47 | 48 | func (v *IAMVirtualMFADevice) Remove() error { 49 | if v.user != nil { 50 | _, err := v.svc.DeactivateMFADevice(&iam.DeactivateMFADeviceInput{ 51 | UserName: v.user.UserName, SerialNumber: &v.serialNumber, 52 | }) 53 | if err != nil { 54 | return err 55 | } 56 | } 57 | 58 | _, err := v.svc.DeleteVirtualMFADevice(&iam.DeleteVirtualMFADeviceInput{ 59 | SerialNumber: &v.serialNumber, 60 | }) 61 | return err 62 | } 63 | 64 | func (v *IAMVirtualMFADevice) String() string { 65 | return v.serialNumber 66 | } 67 | -------------------------------------------------------------------------------- /tools/awsnukedocgen/samples/nuke/resources/sns-platformapplications.go.txt: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws/awserr" 5 | "github.com/aws/aws-sdk-go/aws/session" 6 | "github.com/aws/aws-sdk-go/service/sns" 7 | ) 8 | 9 | type SNSPlatformApplication struct { 10 | svc *sns.SNS 11 | ARN *string 12 | } 13 | 14 | func init() { 15 | register("SNSPlatformApplication", ListSNSPlatformApplications) 16 | } 17 | 18 | func ListSNSPlatformApplications(sess *session.Session) ([]Resource, error) { 19 | svc := sns.New(sess) 20 | resources := []Resource{} 21 | 22 | params := &sns.ListPlatformApplicationsInput{} 23 | 24 | for { 25 | resp, err := svc.ListPlatformApplications(params) 26 | if err != nil { 27 | awsErr, ok := err.(awserr.Error) 28 | if ok && awsErr.Code() == "InvalidAction" && awsErr.Message() == "Operation (ListPlatformApplications) is not supported in this region" { 29 | // AWS answers with InvalidAction on regions that do not 30 | // support ListPlatformApplications. 31 | break 32 | } 33 | 34 | return nil, err 35 | } 36 | 37 | for _, platformApplication := range resp.PlatformApplications { 38 | resources = append(resources, &SNSPlatformApplication{ 39 | svc: svc, 40 | ARN: platformApplication.PlatformApplicationArn, 41 | }) 42 | } 43 | if resp.NextToken == nil { 44 | break 45 | } 46 | 47 | params.NextToken = resp.NextToken 48 | } 49 | return resources, nil 50 | } 51 | 52 | func (f *SNSPlatformApplication) Remove() error { 53 | 54 | _, err := f.svc.DeletePlatformApplication(&sns.DeletePlatformApplicationInput{ 55 | PlatformApplicationArn: f.ARN, 56 | }) 57 | 58 | return err 59 | } 60 | 61 | func (f *SNSPlatformApplication) String() string { 62 | return *f.ARN 63 | } 64 | -------------------------------------------------------------------------------- /pkg/account/mocks/Eventer.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import account "github.com/Optum/dce/pkg/account" 6 | import mock "github.com/stretchr/testify/mock" 7 | 8 | // Eventer is an autogenerated mock type for the Eventer type 9 | type Eventer struct { 10 | mock.Mock 11 | } 12 | 13 | // AccountCreate provides a mock function with given fields: _a0 14 | func (_m *Eventer) AccountCreate(_a0 *account.Account) error { 15 | ret := _m.Called(_a0) 16 | 17 | var r0 error 18 | if rf, ok := ret.Get(0).(func(*account.Account) error); ok { 19 | r0 = rf(_a0) 20 | } else { 21 | r0 = ret.Error(0) 22 | } 23 | 24 | return r0 25 | } 26 | 27 | // AccountDelete provides a mock function with given fields: _a0 28 | func (_m *Eventer) AccountDelete(_a0 *account.Account) error { 29 | ret := _m.Called(_a0) 30 | 31 | var r0 error 32 | if rf, ok := ret.Get(0).(func(*account.Account) error); ok { 33 | r0 = rf(_a0) 34 | } else { 35 | r0 = ret.Error(0) 36 | } 37 | 38 | return r0 39 | } 40 | 41 | // AccountReset provides a mock function with given fields: _a0 42 | func (_m *Eventer) AccountReset(_a0 *account.Account) error { 43 | ret := _m.Called(_a0) 44 | 45 | var r0 error 46 | if rf, ok := ret.Get(0).(func(*account.Account) error); ok { 47 | r0 = rf(_a0) 48 | } else { 49 | r0 = ret.Error(0) 50 | } 51 | 52 | return r0 53 | } 54 | 55 | // AccountUpdate provides a mock function with given fields: old, new 56 | func (_m *Eventer) AccountUpdate(old *account.Account, new *account.Account) error { 57 | ret := _m.Called(old, new) 58 | 59 | var r0 error 60 | if rf, ok := ret.Get(0).(func(*account.Account, *account.Account) error); ok { 61 | r0 = rf(old, new) 62 | } else { 63 | r0 = ret.Error(0) 64 | } 65 | 66 | return r0 67 | } 68 | -------------------------------------------------------------------------------- /pkg/reset/athena_test.go: -------------------------------------------------------------------------------- 1 | package reset 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aws/aws-sdk-go/aws" 7 | "github.com/aws/aws-sdk-go/service/athena" 8 | "github.com/aws/aws-sdk-go/service/athena/athenaiface" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | type mockAthenaReset struct { 13 | athenaiface.AthenaAPI 14 | } 15 | 16 | // ListWorkGroups implemenation 17 | func (athenaReset mockAthenaReset) ListWorkGroups(input *athena.ListWorkGroupsInput) (*athena.ListWorkGroupsOutput, error) { 18 | wg1 := &athena.WorkGroupSummary{ 19 | Name: aws.String("wg1"), 20 | } 21 | mockOutput := &athena.ListWorkGroupsOutput{ 22 | WorkGroups: []*athena.WorkGroupSummary{wg1}, 23 | } 24 | return mockOutput, nil 25 | } 26 | 27 | // ListNamedQueries implemenation 28 | func (athenaReset mockAthenaReset) ListNamedQueries(input *athena.ListNamedQueriesInput) (*athena.ListNamedQueriesOutput, error) { 29 | mockOutput := &athena.ListNamedQueriesOutput{ 30 | NamedQueryIds: []*string{aws.String("test-query-1"), aws.String("test-query-2")}, 31 | } 32 | return mockOutput, nil 33 | } 34 | 35 | // DeleteWorkGroup implemenation 36 | func (athenaReset mockAthenaReset) DeleteWorkGroup(input *athena.DeleteWorkGroupInput) (*athena.DeleteWorkGroupOutput, error) { 37 | mockOutput := &athena.DeleteWorkGroupOutput{} 38 | return mockOutput, nil 39 | } 40 | 41 | // DeleteNamedQuery implemenation 42 | func (athenaReset mockAthenaReset) DeleteNamedQuery(input *athena.DeleteNamedQueryInput) (*athena.DeleteNamedQueryOutput, error) { 43 | mockOutput := &athena.DeleteNamedQueryOutput{} 44 | return mockOutput, nil 45 | } 46 | 47 | func TestDeleteAthenaResources(t *testing.T) { 48 | mockAthena := new(mockAthenaReset) 49 | err := DeleteAthenaResources(mockAthena) 50 | assert.Nil(t, err, "There should be no errors") 51 | } 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Project 2 | bin 3 | build 4 | bin/* 5 | local_terraform.tfvars 6 | 7 | # Sphinx build 8 | _build/ 9 | 10 | ## Documentation 11 | docs/CHANGELOG.md 12 | docs/CONTRIBUTING.md 13 | docs/CODE_OF_CONDUCT.md 14 | docs/INDIVIDUAL_CONTRIBUTOR_LICENSE.md 15 | # The API documentation is also generated. 16 | docs/api-documentation.md 17 | /html-docs 18 | docs/dce/ 19 | 20 | ## Terraform 21 | # Local .terraform directories 22 | **/.terraform/* 23 | 24 | # .tfstate files 25 | *.tfstate 26 | *.tfstate.* 27 | 28 | # Crash log files 29 | crash.log 30 | 31 | # Ignore any .tfvars files that are generated automatically for each Terraform run. Most 32 | # .tfvars files are managed as part of configuration and so should be included in 33 | # version control. 34 | # 35 | # example.tfvars 36 | 37 | # Ignore override files as they are usually used to override resources locally and so 38 | # are not checked in 39 | override.tf 40 | override.tf.json 41 | *_override.tf 42 | *_override.tf.json 43 | 44 | # Include override files you do wish to add to version control using negated pattern 45 | # 46 | # !example_override.tf 47 | 48 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 49 | # example: *tfplan* 50 | 51 | ## Go 52 | # Binaries for programs and plugins 53 | *.exe 54 | *.exe~ 55 | *.dll 56 | *.so 57 | *.dylib 58 | 59 | # Test binary, built with `go test -c` 60 | *.test 61 | 62 | # Output of the go coverage tool, specifically when used with LiteIDE 63 | *.out 64 | 65 | # Other stuff 66 | *.zip 67 | .idea/ 68 | 69 | junit-report/ 70 | test.output.txt 71 | coverage.json 72 | coverage.txt 73 | coverage.xml 74 | tf-lsp.log 75 | 76 | # Python 77 | 78 | # Environments 79 | .env 80 | .venv 81 | env/ 82 | venv/ 83 | ENV/ 84 | env.bak/ 85 | venv.bak/ 86 | 87 | # mkdocs documentation 88 | /site 89 | 90 | .vscode/ 91 | 92 | tests/integration/test.cfg -------------------------------------------------------------------------------- /tools/awsnukedocgen/samples/nuke/resources/elb-elb.go.txt: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws/session" 5 | "github.com/aws/aws-sdk-go/service/elb" 6 | "github.com/rebuy-de/aws-nuke/pkg/types" 7 | ) 8 | 9 | type ELBLoadBalancer struct { 10 | svc *elb.ELB 11 | name *string 12 | tags []*elb.Tag 13 | } 14 | 15 | func init() { 16 | register("ELB", ListELBLoadBalancers) 17 | } 18 | 19 | func ListELBLoadBalancers(sess *session.Session) ([]Resource, error) { 20 | resources := make([]Resource, 0) 21 | 22 | svc := elb.New(sess) 23 | 24 | elbResp, err := svc.DescribeLoadBalancers(nil) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | for _, elbLoadBalancer := range elbResp.LoadBalancerDescriptions { 30 | // Tags for ELBs need to be fetched separately 31 | tagResp, err := svc.DescribeTags(&elb.DescribeTagsInput{ 32 | LoadBalancerNames: []*string{elbLoadBalancer.LoadBalancerName}, 33 | }) 34 | 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | for _, elbTagInfo := range tagResp.TagDescriptions { 40 | resources = append(resources, &ELBLoadBalancer{ 41 | svc: svc, 42 | name: elbTagInfo.LoadBalancerName, 43 | tags: elbTagInfo.Tags, 44 | }) 45 | } 46 | } 47 | 48 | return resources, nil 49 | } 50 | 51 | func (e *ELBLoadBalancer) Remove() error { 52 | params := &elb.DeleteLoadBalancerInput{ 53 | LoadBalancerName: e.name, 54 | } 55 | 56 | _, err := e.svc.DeleteLoadBalancer(params) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | return nil 62 | } 63 | 64 | func (e *ELBLoadBalancer) Properties() types.Properties { 65 | properties := types.NewProperties() 66 | for _, tagValue := range e.tags { 67 | properties.SetTag(tagValue.Key, tagValue.Value) 68 | } 69 | return properties 70 | } 71 | 72 | func (e *ELBLoadBalancer) String() string { 73 | return *e.name 74 | } 75 | -------------------------------------------------------------------------------- /pkg/common/token.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/Optum/dce/pkg/awsiface" 5 | "github.com/aws/aws-sdk-go/aws" 6 | "github.com/aws/aws-sdk-go/aws/client" 7 | "github.com/aws/aws-sdk-go/aws/credentials" 8 | "github.com/aws/aws-sdk-go/aws/credentials/stscreds" 9 | "github.com/aws/aws-sdk-go/aws/session" 10 | "github.com/aws/aws-sdk-go/service/sts" 11 | ) 12 | 13 | // TokenService interface requires a method to receive credentials for an AWS 14 | // Role provided by the Role Input. 15 | //go:generate mockery -name TokenService 16 | type TokenService interface { 17 | AssumeRole(*sts.AssumeRoleInput) (*sts.AssumeRoleOutput, error) 18 | NewCredentials(client.ConfigProvider, string) *credentials.Credentials 19 | NewSession(baseSession awsiface.AwsSession, roleArn string) (awsiface.AwsSession, error) 20 | } 21 | 22 | // STS implements the TokenService interface using AWS STS Client 23 | type STS struct { 24 | Client *sts.STS 25 | } 26 | 27 | // AssumeRole returns an STS AssumeRoleOutput struct based on the provided 28 | // input through the AWS STS Client 29 | func (service STS) AssumeRole(input *sts.AssumeRoleInput) ( 30 | *sts.AssumeRoleOutput, error) { 31 | return service.Client.AssumeRole(input) 32 | } 33 | 34 | // NewCredentials returns a set of credentials for an Assume Role 35 | func (service STS) NewCredentials(inputClient client.ConfigProvider, 36 | inputRole string) *credentials.Credentials { 37 | return stscreds.NewCredentials(inputClient, inputRole) 38 | } 39 | 40 | func (service STS) NewSession(baseSession awsiface.AwsSession, roleArn string) (awsiface.AwsSession, error) { 41 | creds := service.NewCredentials(baseSession, roleArn) 42 | newSession, err := session.NewSession(&aws.Config{ 43 | Credentials: creds, 44 | }) 45 | if err != nil { 46 | return nil, err 47 | } 48 | sess := client.ConfigProvider(newSession) 49 | return sess, nil 50 | } 51 | -------------------------------------------------------------------------------- /modules/cloudwatch_dashboard.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | metrics_namespace = "DCE/AccountPool" 3 | vars = { 4 | api_name = aws_api_gateway_rest_api.gateway_api.name 5 | api_stage_name = aws_api_gateway_stage.api.stage_name 6 | region = var.aws_region 7 | codebuild_name = aws_codebuild_project.reset_build.name 8 | accounts_lambda_name = module.accounts_lambda.name 9 | leases_lambda_name = module.leases_lambda.name 10 | lease_auth_lambda_name = module.lease_auth_lambda.name 11 | usage_lambda_name = module.usage_lambda.name 12 | update_lease_status_lambda_name = module.update_lease_status_lambda.name 13 | fan_out_update_lease_status_lambda_name = module.fan_out_update_lease_status_lambda.name 14 | populate_reset_queue_name = module.populate_reset_queue.name 15 | process_reset_queue_name = module.process_reset_queue.name 16 | update_principal_policy_name = module.update_principal_policy.name 17 | error_scraper_query = "fields @timestamp, @message | sort @timestamp desc | filter @message ~= \\/(?i)error/ or @message ~= \\/(?i)failed/ or @message ~= \\/(?i)fork\\\\/exec/ | display @timestamp, @message, @logStream| limit 10" 18 | metrics_namespace_var = local.metrics_namespace 19 | account_pool_metrics_widget_period = var.account_pool_metrics_widget_period 20 | } 21 | } 22 | 23 | resource "aws_cloudwatch_dashboard" "main" { 24 | count = var.cloudwatch_dashboard_toggle == "true" ? 1 : 0 25 | dashboard_name = "DCE-${var.namespace}" 26 | dashboard_body = replace(templatefile("${path.module}/fixtures/dashboards/cloudwatch_dashboard.json", local.vars), "$$$$", "$") 27 | } 28 | -------------------------------------------------------------------------------- /pkg/usage/mocks/ReaderWriter.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | import usage "github.com/Optum/dce/pkg/usage" 7 | 8 | // ReaderWriter is an autogenerated mock type for the ReaderWriter type 9 | type ReaderWriter struct { 10 | mock.Mock 11 | } 12 | 13 | // Get provides a mock function with given fields: startDate, principalID 14 | func (_m *ReaderWriter) Get(startDate int64, principalID string) (*usage.Usage, error) { 15 | ret := _m.Called(startDate, principalID) 16 | 17 | var r0 *usage.Usage 18 | if rf, ok := ret.Get(0).(func(int64, string) *usage.Usage); ok { 19 | r0 = rf(startDate, principalID) 20 | } else { 21 | if ret.Get(0) != nil { 22 | r0 = ret.Get(0).(*usage.Usage) 23 | } 24 | } 25 | 26 | var r1 error 27 | if rf, ok := ret.Get(1).(func(int64, string) error); ok { 28 | r1 = rf(startDate, principalID) 29 | } else { 30 | r1 = ret.Error(1) 31 | } 32 | 33 | return r0, r1 34 | } 35 | 36 | // List provides a mock function with given fields: query 37 | func (_m *ReaderWriter) List(query *usage.Usage) (*usage.Usages, error) { 38 | ret := _m.Called(query) 39 | 40 | var r0 *usage.Usages 41 | if rf, ok := ret.Get(0).(func(*usage.Usage) *usage.Usages); ok { 42 | r0 = rf(query) 43 | } else { 44 | if ret.Get(0) != nil { 45 | r0 = ret.Get(0).(*usage.Usages) 46 | } 47 | } 48 | 49 | var r1 error 50 | if rf, ok := ret.Get(1).(func(*usage.Usage) error); ok { 51 | r1 = rf(query) 52 | } else { 53 | r1 = ret.Error(1) 54 | } 55 | 56 | return r0, r1 57 | } 58 | 59 | // Write provides a mock function with given fields: i 60 | func (_m *ReaderWriter) Write(i *usage.Usage) error { 61 | ret := _m.Called(i) 62 | 63 | var r0 error 64 | if rf, ok := ret.Get(0).(func(*usage.Usage) error); ok { 65 | r0 = rf(i) 66 | } else { 67 | r0 = ret.Error(0) 68 | } 69 | 70 | return r0 71 | } 72 | -------------------------------------------------------------------------------- /modules/artifacts_bucket.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | principal_policy = var.principal_policy == "" ? "${path.module}/fixtures/policies/principal_policy.tmpl" : var.principal_policy 3 | artifact_bucket_name = "${local.account_id}-dce-artifacts-${var.namespace}" 4 | } 5 | 6 | 7 | # Configure an S3 Bucket to hold artifacts 8 | # (eg. application code deployments, etc.) 9 | resource "aws_s3_bucket" "artifacts" { 10 | bucket = local.artifact_bucket_name 11 | 12 | # Allow S3 access logs to be written to this bucket 13 | acl = "log-delivery-write" 14 | 15 | # Allow Terraform to destroy the bucket 16 | # (so ephemeral PR environments can be torn down) 17 | force_destroy = true 18 | 19 | # Encrypt objects by default 20 | server_side_encryption_configuration { 21 | rule { 22 | apply_server_side_encryption_by_default { 23 | sse_algorithm = "AES256" 24 | } 25 | } 26 | } 27 | 28 | versioning { 29 | enabled = true 30 | } 31 | 32 | tags = var.global_tags 33 | } 34 | 35 | # Enforce SSL only access to the bucket 36 | resource "aws_s3_bucket_policy" "reset_codepipeline_source_ssl_policy" { 37 | bucket = aws_s3_bucket.artifacts.id 38 | 39 | policy = < 6 | 7 | 8 | 9 | ## Types of changes 10 | 11 | 15 | 16 | - [ ] Bugfix (non-breaking change which fixes an issue) 17 | - [ ] New feature (non-breaking change which adds functionality) 18 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 19 | - [ ] Refactor (changes to code, which do not change application behavior) 20 | 21 | ## Checklist 22 | 23 | 26 | 27 | - [ ] I have filled out this PR template 28 | - [ ] I have read the [CONTRIBUTING](https://github.com/Optum/dce/blob/master/docs/CONTRIBUTING.md) doc 29 | - [ ] I have added automated tests that prove my fix is effective or that my feature works 30 | - [ ] I have added necessary documentation (`README.md`, inline comments, etc.) 31 | - [ ] I have updated the `CHANGELOG.md` under a `## next` release, with a short summary of my changes 32 | 33 | 34 | ## Relevant Links 35 | 36 | 43 | 44 | 45 | ## Further comments 46 | 47 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /modules/leases_lambda.tf: -------------------------------------------------------------------------------- 1 | module "leases_lambda" { 2 | source = "./lambda" 3 | name = "leases-${var.namespace}" 4 | namespace = var.namespace 5 | description = "API /leases endpoints" 6 | global_tags = var.global_tags 7 | handler = "leases" 8 | alarm_topic_arn = aws_sns_topic.alarms_topic.arn 9 | 10 | environment = { 11 | DEBUG = "false" 12 | NAMESPACE = var.namespace 13 | AWS_CURRENT_REGION = var.aws_region 14 | RESET_SQS_URL = aws_sqs_queue.account_reset.id 15 | ACCOUNT_DB = aws_dynamodb_table.accounts.id 16 | LEASE_DB = aws_dynamodb_table.leases.id 17 | LEASE_ADDED_TOPIC = aws_sns_topic.lease_added.arn 18 | DECOMMISSION_TOPIC = aws_sns_topic.lease_removed.arn 19 | COGNITO_USER_POOL_ID = module.api_gateway_authorizer.user_pool_id 20 | COGNITO_ROLES_ATTRIBUTE_ADMIN_NAME = var.cognito_roles_attribute_admin_name 21 | MAX_LEASE_BUDGET_AMOUNT = var.max_lease_budget_amount 22 | MAX_LEASE_PERIOD = var.max_lease_period 23 | PRINCIPAL_BUDGET_AMOUNT = var.principal_budget_amount 24 | PRINCIPAL_BUDGET_PERIOD = var.principal_budget_period 25 | USAGE_CACHE_DB = aws_dynamodb_table.usage.id 26 | } 27 | } 28 | 29 | resource "aws_sns_topic" "lease_added" { 30 | name = "lease-added-${var.namespace}" 31 | tags = var.global_tags 32 | } 33 | 34 | resource "aws_sns_topic" "lease_removed" { 35 | name = "lease-removed-${var.namespace}" 36 | tags = var.global_tags 37 | } 38 | 39 | 40 | resource "aws_sns_topic" "lease_locked" { 41 | name = "lease-locked-${var.namespace}" 42 | tags = var.global_tags 43 | } 44 | 45 | resource "aws_sns_topic" "lease_unlocked" { 46 | name = "lease-unlocked-${var.namespace}" 47 | tags = var.global_tags 48 | } 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Disposable Cloud EnvironmentTM 2 | 3 | > **DCETM is your playground in the cloud** 4 | 5 | DCE helps you quickly and safely explore the public cloud by managing temporary AWS accounts. 6 | 7 | Common use cases for a public cloud account include: 8 | - Developing, testing, or operating cloud networks and applications 9 | - Improving infrastructure utilization with autoscaling 10 | - Leveraging cloud-native developer tooling 11 | - Exploring data with analytical and machine learning services 12 | - And much [more](https://aws.amazon.com/)! 13 | 14 | DCE users can "lease" an AWS account for a defined period of time and with a limited budget. 15 | 16 | At the end of the lease, or if the lease's budget is reached, the account is wiped clean and returned to the account pool so it may be leased again. 17 | 18 | ## Getting Started & Documentation 19 | 20 | Deploy your own Disposable Cloud Environment by following the [quickstart](./docs/quickstart.md), also available on our documentation website: 21 | 22 | [dce.readthedocs.io](https://dce.readthedocs.io) 23 | 24 | ## DCE CLI 25 | 26 | The easiest way to get started with DCE is with the DCE CLI: 27 | 28 | [github.com/Optum/dce-cli](https://github.com/Optum/dce-cli) 29 | 30 | ```bash 31 | # Deploy DCE 32 | dce system deploy 33 | 34 | # Add an account to the pool 35 | dce accounts add \ 36 | --account-id 123456789012 \ 37 | --admin-role-arn arn:aws:iam::123456789012:role/OrganizationAccountAccessRole 38 | 39 | # Lease an account 40 | dce leases create \ 41 | --principal-id jdoe@example.com \ 42 | --budget-amount 100 --budget-currency USD 43 | 44 | # Login to your account 45 | dce leases login 46 | ``` 47 | 48 | ## Contributing to DCE 49 | 50 | DCE was born at Optum, but belongs to the community. Improve your cloud experience and [open a PR](https://github.com/Optum/dce/pulls). 51 | 52 | [Contributor Guidelines](./CONTRIBUTING.md) 53 | 54 | 55 | ## License 56 | 57 | [Apache License v2.0](./LICENSE) 58 | -------------------------------------------------------------------------------- /pkg/errors/wrap.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import "reflect" 4 | 5 | // Unwrap returns the result of calling the Unwrap method on err, if err's 6 | // type contains an Unwrap method returning error. 7 | // Otherwise, Unwrap returns nil. 8 | func Unwrap(err error) error { 9 | u, ok := err.(interface { 10 | Unwrap() error 11 | }) 12 | if !ok { 13 | return nil 14 | } 15 | return u.Unwrap() 16 | } 17 | 18 | // Is for seeing if an error includes the target error 19 | func Is(err, target error) bool { 20 | if target == nil { 21 | return err == target 22 | } 23 | 24 | isComparable := reflect.TypeOf(target).Comparable() 25 | for { 26 | if isComparable && err == target { 27 | return true 28 | } 29 | if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) { 30 | return true 31 | } 32 | // TODO: consider supporting target.Is(err). This would allow 33 | // user-definable predicates, but also may allow for coping with sloppy 34 | // APIs, thereby making it easier to get away with them. 35 | if err = Unwrap(err); err == nil { 36 | return false 37 | } 38 | } 39 | } 40 | 41 | // As gets the first error in the chain that matches the target 42 | func As(err error, target interface{}) bool { 43 | if target == nil { 44 | panic("errors: target cannot be nil") 45 | } 46 | val := reflect.ValueOf(target) 47 | typ := val.Type() 48 | if typ.Kind() != reflect.Ptr || val.IsNil() { 49 | panic("errors: target must be a non-nil pointer") 50 | } 51 | if e := typ.Elem(); e.Kind() != reflect.Interface && !e.Implements(errorType) { 52 | panic("errors: *target must be interface or implement error") 53 | } 54 | targetType := typ.Elem() 55 | for err != nil { 56 | if reflect.TypeOf(err).AssignableTo(targetType) { 57 | val.Elem().Set(reflect.ValueOf(err)) 58 | return true 59 | } 60 | if x, ok := err.(interface{ As(interface{}) bool }); ok && x.As(target) { 61 | return true 62 | } 63 | err = Unwrap(err) 64 | } 65 | return false 66 | } 67 | 68 | var errorType = reflect.TypeOf((*error)(nil)).Elem() 69 | -------------------------------------------------------------------------------- /cmd/lambda/update_principal_policy/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "log" 7 | 8 | "github.com/Optum/dce/pkg/config" 9 | "github.com/Optum/dce/pkg/errors" 10 | "github.com/Optum/dce/pkg/lease" 11 | "github.com/aws/aws-lambda-go/events" 12 | "github.com/aws/aws-lambda-go/lambda" 13 | ) 14 | 15 | type configuration struct { 16 | Debug string `env:"DEBUG" envDefault:"false"` 17 | } 18 | 19 | var ( 20 | services *config.ServiceBuilder 21 | // Settings - the configuration settings for the controller 22 | settings *configuration 23 | ) 24 | 25 | func init() { 26 | cfgBldr := &config.ConfigurationBuilder{} 27 | settings = &configuration{} 28 | if err := cfgBldr.Unmarshal(settings); err != nil { 29 | log.Fatalf("Could not load configuration: %s", err.Error()) 30 | } 31 | 32 | // load up the values into the various settings... 33 | err := cfgBldr.WithEnv("AWS_CURRENT_REGION", "AWS_CURRENT_REGION", "us-east-1").Build() 34 | if err != nil { 35 | log.Printf("Error: %+v", err) 36 | } 37 | svcBldr := &config.ServiceBuilder{Config: cfgBldr} 38 | 39 | _, err = svcBldr. 40 | WithAccountService(). 41 | Build() 42 | if err != nil { 43 | panic(err) 44 | } 45 | 46 | services = svcBldr 47 | } 48 | 49 | func main() { 50 | lambda.Start(handler) 51 | } 52 | 53 | func handler(ctx context.Context, snsEvent events.SNSEvent) error { 54 | var lease lease.Lease 55 | 56 | for _, record := range snsEvent.Records { 57 | snsRecord := record.SNS 58 | 59 | err := json.Unmarshal([]byte(snsRecord.Message), &lease) 60 | if err != nil { 61 | log.Printf("Failed to read SNS message %s: %s", snsRecord.Message, err.Error()) 62 | return errors.NewInternalServer("unexpected error parsing SNS message", err) 63 | } 64 | 65 | acct, err := services.AccountService().Get(*lease.AccountID) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | err = services.AccountService().UpsertPrincipalAccess(acct) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | } 76 | return nil 77 | } 78 | -------------------------------------------------------------------------------- /pkg/usage/mocks/DBer.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | import time "time" 7 | import usage "github.com/Optum/dce/pkg/usage" 8 | 9 | // DBer is an autogenerated mock type for the DBer type 10 | type DBer struct { 11 | mock.Mock 12 | } 13 | 14 | // GetUsageByDateRange provides a mock function with given fields: startDate, endDate 15 | func (_m *DBer) GetUsageByDateRange(startDate time.Time, endDate time.Time) ([]*usage.Usage, error) { 16 | ret := _m.Called(startDate, endDate) 17 | 18 | var r0 []*usage.Usage 19 | if rf, ok := ret.Get(0).(func(time.Time, time.Time) []*usage.Usage); ok { 20 | r0 = rf(startDate, endDate) 21 | } else { 22 | if ret.Get(0) != nil { 23 | r0 = ret.Get(0).([]*usage.Usage) 24 | } 25 | } 26 | 27 | var r1 error 28 | if rf, ok := ret.Get(1).(func(time.Time, time.Time) error); ok { 29 | r1 = rf(startDate, endDate) 30 | } else { 31 | r1 = ret.Error(1) 32 | } 33 | 34 | return r0, r1 35 | } 36 | 37 | // GetUsageByPrincipal provides a mock function with given fields: startDate, principalID 38 | func (_m *DBer) GetUsageByPrincipal(startDate time.Time, principalID string) ([]*usage.Usage, error) { 39 | ret := _m.Called(startDate, principalID) 40 | 41 | var r0 []*usage.Usage 42 | if rf, ok := ret.Get(0).(func(time.Time, string) []*usage.Usage); ok { 43 | r0 = rf(startDate, principalID) 44 | } else { 45 | if ret.Get(0) != nil { 46 | r0 = ret.Get(0).([]*usage.Usage) 47 | } 48 | } 49 | 50 | var r1 error 51 | if rf, ok := ret.Get(1).(func(time.Time, string) error); ok { 52 | r1 = rf(startDate, principalID) 53 | } else { 54 | r1 = ret.Error(1) 55 | } 56 | 57 | return r0, r1 58 | } 59 | 60 | // PutUsage provides a mock function with given fields: input 61 | func (_m *DBer) PutUsage(input usage.Usage) error { 62 | ret := _m.Called(input) 63 | 64 | var r0 error 65 | if rf, ok := ret.Get(0).(func(usage.Usage) error); ok { 66 | r0 = rf(input) 67 | } else { 68 | r0 = ret.Error(0) 69 | } 70 | 71 | return r0 72 | } 73 | -------------------------------------------------------------------------------- /pkg/event/sqs_test.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import ( 4 | gErrors "errors" 5 | "math" 6 | "testing" 7 | 8 | "github.com/Optum/dce/pkg/awsiface/mocks" 9 | "github.com/Optum/dce/pkg/errors" 10 | "github.com/aws/aws-sdk-go/aws" 11 | "github.com/aws/aws-sdk-go/service/sqs" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestSqs(t *testing.T) { 16 | 17 | type data struct { 18 | Key string `json:"key"` 19 | } 20 | 21 | tests := []struct { 22 | name string 23 | sqsErr error 24 | event interface{} 25 | expectedErr error 26 | expectedMessageBody string 27 | }{ 28 | { 29 | name: "publish sqs event", 30 | sqsErr: nil, 31 | event: data{ 32 | Key: "value", 33 | }, 34 | expectedMessageBody: "{\"key\":\"value\"}", 35 | expectedErr: nil, 36 | }, 37 | { 38 | name: "publish sqs error", 39 | sqsErr: gErrors.New("error"), 40 | event: data{ 41 | Key: "value", 42 | }, 43 | expectedMessageBody: "{\"key\":\"value\"}", 44 | expectedErr: errors.NewInternalServer("unable to send message to sqs", nil), 45 | }, 46 | { 47 | name: "unmarshal error", 48 | sqsErr: nil, 49 | event: math.Inf(1), 50 | expectedErr: errors.NewInternalServer("unable to marshal response", nil), 51 | }, 52 | } 53 | 54 | for _, tt := range tests { 55 | t.Run(tt.name, func(t *testing.T) { 56 | mockSqs := &mocks.SQSAPI{} 57 | eventer, _ := NewSqsEvent(mockSqs, "http://url.com") 58 | 59 | // Mock Publish call 60 | mockSqs.On("SendMessage", 61 | &sqs.SendMessageInput{ 62 | MessageBody: aws.String(tt.expectedMessageBody), 63 | QueueUrl: aws.String("http://url.com"), 64 | }, 65 | ).Return(nil, tt.sqsErr) 66 | 67 | err := eventer.Publish(tt.event) 68 | if tt.expectedErr == tt.sqsErr { 69 | mockSqs.AssertExpectations(t) 70 | } 71 | 72 | if err != nil { 73 | assert.Equal(t, tt.expectedErr.Error(), err.Error()) 74 | } else { 75 | assert.Nil(t, tt.expectedErr) 76 | } 77 | 78 | }) 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Deploy DCE to AWS Master account 4 | # Requires build artifacts to exist in ./bin/ 5 | # Run ./scripts/build.sh to generate artifacts 6 | # 7 | # Usage: 8 | # ./scripts/deploy.sh 9 | # 10 | # Example: 11 | # ./scripts/deploy.sh ./bin/build_artifacts.zip prod 1234567890-dce-artifacts-prod 12 | 13 | set -euxo pipefail 14 | 15 | FILE=$1 16 | namespace=$2 17 | artifactBucket=$3 18 | 19 | # check if build_artifacts.zip exists (generated from 'scripts/build.sh') 20 | if [[ -f "$FILE" ]]; then 21 | # Unzip build_artifacts.zip into the '__artifacts__/' directory 22 | rm -rf __artifacts__ 23 | unzip ${FILE} -d __artifacts__ 24 | 25 | # Find all Lambda artifacts and upload them to the S3 artifact bucket 26 | for i in $(ls -d __artifacts__/lambda/*.zip) 27 | do 28 | mod_name=$(basename ${i} | cut -f 1 -d '.') 29 | fn_name="${mod_name}-${namespace}" 30 | 31 | # Upload zip file to S3 32 | aws s3 cp \ 33 | __artifacts__/lambda/${mod_name}.zip \ 34 | s3://${artifactBucket}/lambda/${mod_name}.zip \ 35 | --sse 36 | 37 | # Point Lambda Fn at the new code on S3 38 | aws lambda update-function-code \ 39 | --function-name ${fn_name} \ 40 | --s3-bucket ${artifactBucket} \ 41 | --s3-key lambda/${mod_name}.zip 42 | 43 | # Publish new Function version 44 | aws lambda publish-version \ 45 | --function-name ${fn_name} 46 | done 47 | 48 | # Upload the Reset CodeBuild Zip to the S3 artifact bucket. CodeBuild should pick this new file up on its next build. 49 | aws s3 cp \ 50 | __artifacts__/codebuild/reset.zip \ 51 | s3://${artifactBucket}/codebuild/reset.zip \ 52 | --sse 53 | 54 | # Delete the '__artifacts__/' directory after uploading to the s3 artifact bucket 55 | rm -rf __artifacts__ 56 | else 57 | echo "[Error] ${FILE} does not exist yet. Run scripts/build.sh to generate it." 58 | exit 1 59 | fi 60 | -------------------------------------------------------------------------------- /pkg/common/mocks/Storager.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // Storager is an autogenerated mock type for the Storager type 8 | type Storager struct { 9 | mock.Mock 10 | } 11 | 12 | // Download provides a mock function with given fields: bukcet, key, filepath 13 | func (_m *Storager) Download(bukcet string, key string, filepath string) error { 14 | ret := _m.Called(bukcet, key, filepath) 15 | 16 | var r0 error 17 | if rf, ok := ret.Get(0).(func(string, string, string) error); ok { 18 | r0 = rf(bukcet, key, filepath) 19 | } else { 20 | r0 = ret.Error(0) 21 | } 22 | 23 | return r0 24 | } 25 | 26 | // GetObject provides a mock function with given fields: bucket, key 27 | func (_m *Storager) GetObject(bucket string, key string) (string, error) { 28 | ret := _m.Called(bucket, key) 29 | 30 | var r0 string 31 | if rf, ok := ret.Get(0).(func(string, string) string); ok { 32 | r0 = rf(bucket, key) 33 | } else { 34 | r0 = ret.Get(0).(string) 35 | } 36 | 37 | var r1 error 38 | if rf, ok := ret.Get(1).(func(string, string) error); ok { 39 | r1 = rf(bucket, key) 40 | } else { 41 | r1 = ret.Error(1) 42 | } 43 | 44 | return r0, r1 45 | } 46 | 47 | // GetTemplateObject provides a mock function with given fields: bucket, key, input 48 | func (_m *Storager) GetTemplateObject(bucket string, key string, input interface{}) (string, string, error) { 49 | ret := _m.Called(bucket, key, input) 50 | 51 | var r0 string 52 | if rf, ok := ret.Get(0).(func(string, string, interface{}) string); ok { 53 | r0 = rf(bucket, key, input) 54 | } else { 55 | r0 = ret.Get(0).(string) 56 | } 57 | 58 | var r1 string 59 | if rf, ok := ret.Get(1).(func(string, string, interface{}) string); ok { 60 | r1 = rf(bucket, key, input) 61 | } else { 62 | r1 = ret.Get(1).(string) 63 | } 64 | 65 | var r2 error 66 | if rf, ok := ret.Get(2).(func(string, string, interface{}) error); ok { 67 | r2 = rf(bucket, key, input) 68 | } else { 69 | r2 = ret.Error(2) 70 | } 71 | 72 | return r0, r1, r2 73 | } 74 | -------------------------------------------------------------------------------- /pkg/common/notification.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/aws/aws-sdk-go/service/sns" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | // Notificationer interface requires methods to interact with an AWS SNS Topic 11 | // and publish a message to it 12 | type Notificationer interface { 13 | PublishMessage(topicArn *string, message *string, isJSON bool) (*string, error) 14 | } 15 | 16 | // SNS implements the Notification interface with AWS SNS SDK 17 | type SNS struct { 18 | Client *sns.SNS 19 | } 20 | 21 | // PublishMessage pushes the provided messeage to an SNS Topic and returns the 22 | // messages' ID. 23 | func (notif *SNS) PublishMessage(topicArn *string, message *string, 24 | isJSON bool) (*string, error) { 25 | // Create the SNS PublishInput 26 | var publishInput *sns.PublishInput 27 | if isJSON { 28 | messageStructure := "json" 29 | publishInput = &sns.PublishInput{ 30 | TopicArn: topicArn, 31 | Message: message, 32 | MessageStructure: &messageStructure, 33 | } 34 | } else { 35 | publishInput = &sns.PublishInput{ 36 | TopicArn: topicArn, 37 | Message: message, 38 | } 39 | } 40 | 41 | // Publish the Message to the Topic 42 | publishOutput, err := notif.Client.Publish(publishInput) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | return publishOutput.MessageId, err 48 | } 49 | 50 | // PrepareSNSMessageJSON creates a JSON message 51 | // from a struct, for publising to an SNS topic 52 | func PrepareSNSMessageJSON(body interface{}) (string, error) { 53 | // Marshal the message body as JSON 54 | bodyJSON, err := json.Marshal(body) 55 | if err != nil { 56 | return "", errors.Wrap(err, "Failed to prepare SNS body JSON") 57 | } 58 | 59 | // Wrap the body in a SNS message object 60 | messageJSON, err := json.Marshal(struct { 61 | Default string `json:"default"` 62 | Body string `json:"Body"` 63 | }{ 64 | Default: string(bodyJSON), 65 | Body: string(bodyJSON), 66 | }) 67 | if err != nil { 68 | return "", errors.Wrap(err, "Failed to prepare SNS body JSON") 69 | } 70 | 71 | return string(messageJSON), nil 72 | } 73 | -------------------------------------------------------------------------------- /modules/accounts_lambda.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | principal_role_name = "DCEPrincipal${var.namespace == "prod" ? "" : "-${var.namespace}"}" 3 | principal_policy_name = "DCEPrincipalDefaultPolicy${var.namespace == "prod" ? "" : "-${var.namespace}"}" 4 | } 5 | 6 | module "accounts_lambda" { 7 | source = "./lambda" 8 | name = "accounts-${var.namespace}" 9 | namespace = var.namespace 10 | description = "Handles API requests to the /accounts endpoint" 11 | global_tags = var.global_tags 12 | handler = "accounts" 13 | alarm_topic_arn = aws_sns_topic.alarms_topic.arn 14 | 15 | environment = { 16 | DEBUG = "false" 17 | ACCOUNT_ID = local.account_id 18 | NAMESPACE = var.namespace 19 | AWS_CURRENT_REGION = var.aws_region 20 | ACCOUNT_DB = aws_dynamodb_table.accounts.id 21 | ARTIFACTS_BUCKET = aws_s3_bucket.artifacts.id 22 | LEASE_DB = aws_dynamodb_table.leases.id 23 | RESET_SQS_URL = aws_sqs_queue.account_reset.id 24 | ACCOUNT_CREATED_TOPIC_ARN = aws_sns_topic.account_created.arn 25 | ACCOUNT_DELETED_TOPIC_ARN = aws_sns_topic.account_deleted.arn 26 | PRINCIPAL_ROLE_NAME = local.principal_role_name 27 | PRINCIPAL_POLICY_NAME = local.principal_policy_name 28 | PRINCIPAL_IAM_DENY_TAGS = join(",", var.principal_iam_deny_tags) 29 | ALLOWED_REGIONS = join(",", var.allowed_regions) 30 | PRINCIPAL_MAX_SESSION_DURATION = 14400 31 | TAG_ENVIRONMENT = var.namespace == "prod" ? "PROD" : "NON-PROD" 32 | TAG_APP_NAME = lookup(var.global_tags, "AppName") 33 | PRINCIPAL_POLICY_S3_KEY = aws_s3_bucket_object.principal_policy.key 34 | } 35 | } 36 | 37 | resource "aws_sns_topic" "account_created" { 38 | name = "account-created-${var.namespace}" 39 | tags = var.global_tags 40 | } 41 | 42 | resource "aws_sns_topic" "account_deleted" { 43 | name = "account-deleted-${var.namespace}" 44 | tags = var.global_tags 45 | } 46 | -------------------------------------------------------------------------------- /pkg/lease/mocks/Reader.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import lease "github.com/Optum/dce/pkg/lease" 6 | import mock "github.com/stretchr/testify/mock" 7 | 8 | // Reader is an autogenerated mock type for the Reader type 9 | type Reader struct { 10 | mock.Mock 11 | } 12 | 13 | // Get provides a mock function with given fields: leaseID 14 | func (_m *Reader) Get(leaseID string) (*lease.Lease, error) { 15 | ret := _m.Called(leaseID) 16 | 17 | var r0 *lease.Lease 18 | if rf, ok := ret.Get(0).(func(string) *lease.Lease); ok { 19 | r0 = rf(leaseID) 20 | } else { 21 | if ret.Get(0) != nil { 22 | r0 = ret.Get(0).(*lease.Lease) 23 | } 24 | } 25 | 26 | var r1 error 27 | if rf, ok := ret.Get(1).(func(string) error); ok { 28 | r1 = rf(leaseID) 29 | } else { 30 | r1 = ret.Error(1) 31 | } 32 | 33 | return r0, r1 34 | } 35 | 36 | // GetByAccountIDAndPrincipalID provides a mock function with given fields: accountID, principalID 37 | func (_m *Reader) GetByAccountIDAndPrincipalID(accountID string, principalID string) (*lease.Lease, error) { 38 | ret := _m.Called(accountID, principalID) 39 | 40 | var r0 *lease.Lease 41 | if rf, ok := ret.Get(0).(func(string, string) *lease.Lease); ok { 42 | r0 = rf(accountID, principalID) 43 | } else { 44 | if ret.Get(0) != nil { 45 | r0 = ret.Get(0).(*lease.Lease) 46 | } 47 | } 48 | 49 | var r1 error 50 | if rf, ok := ret.Get(1).(func(string, string) error); ok { 51 | r1 = rf(accountID, principalID) 52 | } else { 53 | r1 = ret.Error(1) 54 | } 55 | 56 | return r0, r1 57 | } 58 | 59 | // List provides a mock function with given fields: _a0 60 | func (_m *Reader) List(_a0 *lease.Lease) (*lease.Leases, error) { 61 | ret := _m.Called(_a0) 62 | 63 | var r0 *lease.Leases 64 | if rf, ok := ret.Get(0).(func(*lease.Lease) *lease.Leases); ok { 65 | r0 = rf(_a0) 66 | } else { 67 | if ret.Get(0) != nil { 68 | r0 = ret.Get(0).(*lease.Leases) 69 | } 70 | } 71 | 72 | var r1 error 73 | if rf, ok := ret.Get(1).(func(*lease.Lease) error); ok { 74 | r1 = rf(_a0) 75 | } else { 76 | r1 = ret.Error(1) 77 | } 78 | 79 | return r0, r1 80 | } 81 | -------------------------------------------------------------------------------- /pkg/common/queue.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws" 5 | "github.com/aws/aws-sdk-go/aws/session" 6 | "github.com/aws/aws-sdk-go/service/sqs" 7 | ) 8 | 9 | // Queue interface requires a method to receive an SQS Message Output based on 10 | // the provided SQS Message Input 11 | type Queue interface { 12 | SendMessage(*string, *string) error 13 | ReceiveMessage(*sqs.ReceiveMessageInput) (*sqs.ReceiveMessageOutput, error) 14 | DeleteMessage(*sqs.DeleteMessageInput) (*sqs.DeleteMessageOutput, error) 15 | NewFromEnv() error 16 | } 17 | 18 | // SQSQueue implments the Queue interface using the AWS SQS Service 19 | type SQSQueue struct { 20 | Client *sqs.SQS 21 | } 22 | 23 | // SendMessage sends the provided message to the queue using the AWS SQS Service 24 | // and returns an errors 25 | func (queue SQSQueue) SendMessage(queueURL *string, message *string) error { 26 | // Create the input 27 | input := sqs.SendMessageInput{ 28 | QueueUrl: queueURL, 29 | MessageBody: message, 30 | } 31 | 32 | // Send the message 33 | _, err := queue.Client.SendMessage(&input) 34 | return err 35 | } 36 | 37 | // ReceiveMessage method returns an AWS SQS Message Output based on the provided 38 | // Message Input through the AWS SQS Client. 39 | func (queue SQSQueue) ReceiveMessage(input *sqs.ReceiveMessageInput) ( 40 | *sqs.ReceiveMessageOutput, error) { 41 | return queue.Client.ReceiveMessage(input) 42 | } 43 | 44 | // DeleteMessage method returns an AWS SQS Delete Message Output based on the 45 | // provided Delete Message Input through the SQS Client 46 | func (queue SQSQueue) DeleteMessage(input *sqs.DeleteMessageInput) ( 47 | *sqs.DeleteMessageOutput, error) { 48 | return queue.Client.DeleteMessage(input) 49 | } 50 | 51 | // NewFromEnv creates an SQS instance configured from environment variables. 52 | // Requires env vars for: 53 | // - AWS_CURRENT_REGION 54 | func (queue SQSQueue) NewFromEnv() error { 55 | awsSession, err := session.NewSession(&aws.Config{ 56 | Region: aws.String(RequireEnv("AWS_CURRENT_REGION"))}) 57 | if err != nil { 58 | return err 59 | } 60 | queue.Client = sqs.New(awsSession) 61 | 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /tools/awsnukedocgen/samples/nuke/resources/s3-multipart-uploads.go.txt: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/aws/aws-sdk-go/aws/session" 7 | "github.com/aws/aws-sdk-go/service/s3" 8 | "github.com/rebuy-de/aws-nuke/pkg/types" 9 | ) 10 | 11 | type S3MultipartUpload struct { 12 | svc *s3.S3 13 | bucket string 14 | key string 15 | uploadID string 16 | } 17 | 18 | func init() { 19 | register("S3MultipartUpload", ListS3MultipartUpload) 20 | } 21 | 22 | func ListS3MultipartUpload(sess *session.Session) ([]Resource, error) { 23 | svc := s3.New(sess) 24 | 25 | resources := make([]Resource, 0) 26 | 27 | buckets, err := DescribeS3Buckets(svc) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | for _, name := range buckets { 33 | params := &s3.ListMultipartUploadsInput{ 34 | Bucket: &name, 35 | } 36 | 37 | for { 38 | resp, err := svc.ListMultipartUploads(params) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | for _, upload := range resp.Uploads { 44 | if upload.Key == nil || upload.UploadId == nil { 45 | continue 46 | } 47 | 48 | resources = append(resources, &S3MultipartUpload{ 49 | svc: svc, 50 | bucket: name, 51 | key: *upload.Key, 52 | uploadID: *upload.UploadId, 53 | }) 54 | } 55 | 56 | if *resp.IsTruncated { 57 | params.KeyMarker = resp.NextKeyMarker 58 | continue 59 | } 60 | 61 | break 62 | } 63 | } 64 | 65 | return resources, nil 66 | } 67 | 68 | func (e *S3MultipartUpload) Remove() error { 69 | params := &s3.AbortMultipartUploadInput{ 70 | Bucket: &e.bucket, 71 | Key: &e.key, 72 | UploadId: &e.uploadID, 73 | } 74 | 75 | _, err := e.svc.AbortMultipartUpload(params) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | return nil 81 | } 82 | 83 | func (e *S3MultipartUpload) Properties() types.Properties { 84 | return types.NewProperties(). 85 | Set("Bucket", e.bucket). 86 | Set("Key", e.key). 87 | Set("UploadID", e.uploadID) 88 | } 89 | 90 | func (e *S3MultipartUpload) String() string { 91 | return fmt.Sprintf("s3://%s/%s#%s", e.bucket, e.key, e.uploadID) 92 | } 93 | -------------------------------------------------------------------------------- /pkg/data/helpers.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | 7 | "github.com/aws/aws-sdk-go/service/dynamodb" 8 | "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface" 9 | "github.com/aws/aws-sdk-go/service/dynamodb/expression" 10 | ) 11 | 12 | type stringer interface { 13 | String() string 14 | } 15 | 16 | func getFiltersFromStruct(input interface{}, keyName *string) (*expression.KeyConditionBuilder, *expression.ConditionBuilder) { 17 | var cb *expression.ConditionBuilder 18 | var kb *expression.KeyConditionBuilder 19 | var dValue interface{} 20 | v := reflect.ValueOf(input).Elem() 21 | for i := 0; i < v.NumField(); i++ { 22 | dField := strings.Split(v.Type().Field(i).Tag.Get("dynamodbav"), ",")[0] 23 | if dField != "-" { 24 | value := v.Field(i).Interface() 25 | if !reflect.ValueOf(value).IsNil() { 26 | t := v.Field(i) 27 | switch t.Kind() { 28 | case reflect.Ptr: 29 | if u, ok := t.Interface().(stringer); ok { 30 | dValue = u.String() 31 | } else { 32 | dValue = reflect.Indirect(v.Field(i)).Interface() 33 | } 34 | if keyName != nil { 35 | if dField == *keyName { 36 | newFilter := expression.Key(dField).Equal(expression.Value(dValue)) 37 | kb = &newFilter 38 | continue 39 | } 40 | } 41 | if cb == nil { 42 | newFilter := expression.Name(dField).Equal(expression.Value(dValue)) 43 | cb = &newFilter 44 | } else { 45 | *cb = cb.And(expression.Name(dField).Equal(expression.Value(dValue))) 46 | } 47 | } 48 | } 49 | } 50 | } 51 | return kb, cb 52 | } 53 | 54 | func putItem(input *dynamodb.PutItemInput, dataInterface dynamodbiface.DynamoDBAPI) error { 55 | _, err := dataInterface.PutItem(input) 56 | return err 57 | } 58 | 59 | func query(input *dynamodb.QueryInput, dataInterface dynamodbiface.DynamoDBAPI) (*dynamodb.QueryOutput, error) { 60 | output, err := dataInterface.Query(input) 61 | return output, err 62 | } 63 | 64 | func getItem(input *dynamodb.GetItemInput, dataInterface dynamodbiface.DynamoDBAPI) (*dynamodb.GetItemOutput, error) { 65 | output, err := dataInterface.GetItem(input) 66 | return output, err 67 | } 68 | -------------------------------------------------------------------------------- /docs/home.md: -------------------------------------------------------------------------------- 1 | # Disposable Cloud Environment (DCE)TM 2 | 3 | The Disposable Cloud Environment (DCE) provides temporary, limited Amazon Web Services (AWS) accounts. Accounts can be "leased" for a period of time or up to a pre-determined budget amount. When the period of time is reached or the maximum budgeted amount is exceeded, the lease is expired. The leased account is _reset_ and returned to a pool of accounts to be leased again. 4 | 5 | At a high-level, DCE consists of [AWS Lambda](https://aws.amazon.com/lambda/) functions (implemented in [Go](https://golang.org/)), 6 | [Amazon DynamoDB](https://aws.amazon.com/dynamodb/) tables, 7 | [Amazon Simple Notification Service (SNS)](https://aws.amazon.com/sns/) topics, 8 | and APIs exposed with [Amazon API Gateway](https://aws.amazon.com/api-gateway/). 9 | These resources are created in the AWS account using [Hashicorp Terraform](https://www.terraform.io/). 10 | 11 | ## Why DCE? 12 | 13 | > **DCE is your playground in the cloud** 14 | 15 | With a DCE account, you have a safe environment to experiment with in the 16 | cloud. With near-administrative access to an AWS account, you 17 | can click around the AWS web console or run AWS CLI commands from your terminal. 18 | As a developer in an organization, you don't need to worry about cost management or orphaned resources. 19 | 20 | ### Budget limits 21 | 22 | DCE can be configured to expire account _leases_ (see [Concepts documentation](concepts.md)) 23 | based on a budgeted amount for usage. 24 | 25 | Once the account hits a weekly spending limit, the account will be automatically 26 | wiped clean so there are no surprise bills at the end of the month. 27 | 28 | ### Timed leases 29 | 30 | DCE contains the concept of _expiring leases_. A _lease_ represents temporary 31 | access to an account for a certain amount of time. Once the lease is expired, 32 | the AWS account is cleaned of all of the resources and returned to the pool 33 | for other people to lease. 34 | 35 | ## Getting started 36 | 37 | To get started using DCE, see the [quickstart](quickstart.md). 38 | 39 | ## Viewing the source 40 | 41 | The source code for DCE can be found on [GitHub](https://github.com/Optum/dce). 42 | -------------------------------------------------------------------------------- /pkg/budget/budget.go: -------------------------------------------------------------------------------- 1 | package budget 2 | 3 | import ( 4 | "github.com/Optum/dce/pkg/awsiface" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/aws/aws-sdk-go/aws" 9 | "github.com/aws/aws-sdk-go/service/costexplorer" 10 | ) 11 | 12 | // Define a Service, so we can mock this service from other components 13 | // (eg, if I'm testing a Lambda controller that uses this Service) 14 | //go:generate mockery -name Service 15 | type Service interface { 16 | CalculateTotalSpend(startDate time.Time, endDate time.Time) (float64, error) 17 | SetCostExplorer(costExplorer awsiface.CostExplorerAPI) 18 | } 19 | 20 | // Define a concrete implementation of the Service interface 21 | type AWSBudgetService struct { 22 | CostExplorer awsiface.CostExplorerAPI 23 | } 24 | 25 | func (budgetSvc *AWSBudgetService) SetCostExplorer(costExplorer awsiface.CostExplorerAPI) { 26 | budgetSvc.CostExplorer = costExplorer 27 | } 28 | 29 | // Implement the CalculateTotalSpend method of the Service interface 30 | func (budgetSvc *AWSBudgetService) CalculateTotalSpend(startDate time.Time, endDate time.Time) (float64, error) { 31 | 32 | // CostExplorer uses strings for dates, in the format 33 | // of "2017-01-01" 34 | // Golang time formatting syntax is confusing as can be. 35 | // See https://stackoverflow.com/a/20234207 36 | timeFormat := "2006-01-02" 37 | timePeriod := costexplorer.DateInterval{ 38 | Start: aws.String(startDate.UTC().Format(timeFormat)), 39 | End: aws.String(endDate.UTC().Format(timeFormat)), 40 | } 41 | 42 | metrics := []*string{aws.String("UnblendedCost")} 43 | granularity := aws.String("DAILY") 44 | 45 | getCostAndUsageInput := costexplorer.GetCostAndUsageInput{ 46 | Metrics: metrics, 47 | TimePeriod: &timePeriod, 48 | Granularity: granularity, 49 | } 50 | 51 | output, err := budgetSvc.CostExplorer.GetCostAndUsage(&getCostAndUsageInput) 52 | if err != nil { 53 | return 0, err 54 | } 55 | 56 | var totalCost float64 57 | 58 | for _, result := range output.ResultsByTime { 59 | cost, err := strconv.ParseFloat(*result.Total["UnblendedCost"].Amount, 64) 60 | if err != nil { 61 | return 0, err 62 | } 63 | 64 | totalCost = totalCost + cost 65 | 66 | } 67 | return totalCost, nil 68 | } 69 | --------------------------------------------------------------------------------