├── .eslintignore ├── .eslintrc.json ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── actions │ └── build │ │ └── action.yml ├── dependabot.yml └── workflows │ ├── package.yml │ ├── release.yml │ └── tests.yml ├── .gitignore ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── __integration_tests__ ├── name_transformation.base.ts ├── name_transformation │ ├── lowercase.test.ts │ ├── none.test.ts │ └── uppercase.test.ts └── parse_json_secrets.test.ts ├── __tests__ ├── cleanup.test.ts ├── index.test.ts └── utils.test.ts ├── action.yml ├── dist ├── cleanup.js ├── cleanup │ └── index.js ├── constants.js ├── index.js ├── index.js.map ├── licenses.txt ├── sourcemap-register.js └── utils.js ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── cleanup.ts ├── constants.ts ├── index.ts └── utils.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "@typescript-eslint" 6 | ], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended" 11 | ] 12 | } -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. -------------------------------------------------------------------------------- /.github/actions/build/action.yml: -------------------------------------------------------------------------------- 1 | name: Builds 2 | description: Builds the repository and assumes the AWS IAM role for testing 3 | inputs: 4 | aws-region: 5 | description: Region where the credentials will be assumed 6 | required: false 7 | default: us-east-1 8 | runs: 9 | using: composite 10 | steps: 11 | - name: Install dependencies 12 | run: npm ci 13 | shell: bash 14 | - name: Build the dist folder 15 | run: npm run build 16 | shell: bash 17 | - name: Determine role to assume 18 | id: role-to-assume 19 | run: | 20 | if [ "${{ github.repository_owner }}" == "aws-actions" ]; then 21 | # Use prod role for the PRs running against the main repo 22 | echo "arn=arn:aws:iam::339713045997:role/GithubActionsRole" >> "$GITHUB_OUTPUT" 23 | else 24 | # Use beta role for the PRs running against engineer forks 25 | echo "arn=arn:aws:iam::654654453185:role/GithubActionsRole" >> "$GITHUB_OUTPUT" 26 | fi 27 | shell: bash 28 | - name: Configure AWS Credentials 29 | uses: aws-actions/configure-aws-credentials@v4 30 | with: 31 | role-to-assume: ${{ steps.role-to-assume.outputs.arn }} 32 | aws-region: ${{ inputs.aws-region }} -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | groups: 13 | dependencies: 14 | applies-to: version-updates 15 | dependency-type: production 16 | update-types: 17 | - minor 18 | - patch 19 | dev-dependencies: 20 | applies-to: version-updates 21 | dependency-type: development 22 | update-types: 23 | - minor 24 | - patch 25 | - package-ecosystem: "github-actions" 26 | directory: "/" 27 | schedule: 28 | interval: "weekly" 29 | -------------------------------------------------------------------------------- /.github/workflows/package.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | 6 | name: Package 7 | 8 | jobs: 9 | check: 10 | name: Package distribution file 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | with: 16 | ref: main 17 | - name: Package 18 | run: | 19 | npm ci 20 | npm test 21 | npm run build 22 | - name: Commit 23 | run: | 24 | git config user.name "$(git log -n 1 --pretty=format:%an)" 25 | git config user.email "$(git log -n 1 --pretty=format:%ae)" 26 | git add dist/ 27 | git commit -m "chore: Update dist" || echo "No changes to commit" 28 | git push origin HEAD:main 29 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Update Major Release Tag 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | update-tag: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Get major version num and update tag 13 | run: | 14 | VERSION=${GITHUB_REF#refs/tags/} 15 | MAJOR=${VERSION%%.*} 16 | git config user.name "$(git log -n 1 --pretty=format:%an)" 17 | git config user.email "$(git log -n 1 --pretty=format:%ae)" 18 | echo "Updating ${MAJOR} tag" 19 | git tag -fa ${MAJOR} -m "Update major version tag" 20 | git push origin ${MAJOR} --force -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | push: 8 | branches: 9 | - main 10 | 11 | permissions: 12 | id-token: write 13 | contents: read 14 | 15 | jobs: 16 | unit-tests: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | - name: Run Unit Tests 22 | run: | 23 | npm ci 24 | npm run test 25 | - name: Codecov 26 | uses: codecov/codecov-action@v5.4.2 27 | env: 28 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 29 | 30 | uppercase-transformation-integration-test: 31 | runs-on: ubuntu-latest 32 | needs: unit-tests 33 | steps: 34 | - name: Checkout 35 | uses: actions/checkout@v4 36 | - name: Build 37 | uses: ./.github/actions/build 38 | - name: Act 39 | uses: ./ 40 | with: 41 | name-transformation: uppercase 42 | parse-json-secrets: true 43 | secret-ids: | 44 | SampleSecret1 45 | /special/chars/secret 46 | 0/special/chars/secret 47 | PrefixSecret* 48 | JsonSecret 49 | SAMPLESECRET1_ALIAS, SampleSecret1 50 | - name: Assert 51 | run: npm run integration-test __integration_tests__/name_transformation/uppercase.test.ts 52 | 53 | lowercase-transformation-integration-test: 54 | runs-on: ubuntu-latest 55 | needs: unit-tests 56 | steps: 57 | - name: Checkout 58 | uses: actions/checkout@v4 59 | - name: Build 60 | uses: ./.github/actions/build 61 | - name: Act 62 | uses: ./ 63 | with: 64 | name-transformation: lowercase 65 | parse-json-secrets: true 66 | secret-ids: | 67 | SampleSecret1 68 | /special/chars/secret 69 | 0/special/chars/secret 70 | PrefixSecret* 71 | JsonSecret 72 | samplesecret1_alias, SampleSecret1 73 | - name: Assert 74 | run: npm run integration-test __integration_tests__/name_transformation/lowercase.test.ts 75 | 76 | none-transformation-integration-test: 77 | runs-on: ubuntu-latest 78 | needs: unit-tests 79 | steps: 80 | - name: Checkout 81 | uses: actions/checkout@v4 82 | - name: Build 83 | uses: ./.github/actions/build 84 | - name: Act 85 | uses: ./ 86 | with: 87 | name-transformation: none 88 | parse-json-secrets: true 89 | secret-ids: | 90 | SampleSecret1 91 | /special/chars/secret 92 | 0/special/chars/secret 93 | PrefixSecret* 94 | JsonSecret 95 | SampleSecret1_Alias, SampleSecret1 96 | - name: Assert 97 | run: npm run integration-test __integration_tests__/name_transformation/none.test.ts 98 | 99 | default-name-transformation-param-integration-test: 100 | runs-on: ubuntu-latest 101 | needs: unit-tests 102 | steps: 103 | - name: Checkout 104 | uses: actions/checkout@v4 105 | - name: Build 106 | uses: ./.github/actions/build 107 | - name: Act 108 | uses: ./ 109 | with: 110 | parse-json-secrets: true 111 | secret-ids: | 112 | SampleSecret1 113 | /special/chars/secret 114 | 0/special/chars/secret 115 | PrefixSecret* 116 | JsonSecret 117 | SAMPLESECRET1_ALIAS, SampleSecret1 118 | - name: Assert 119 | run: npm run integration-test __integration_tests__/name_transformation/uppercase.test.ts 120 | 121 | default-parse-json-secrets-integration-test: 122 | runs-on: ubuntu-latest 123 | needs: unit-tests 124 | steps: 125 | - name: Checkout 126 | uses: actions/checkout@v4 127 | - name: Build 128 | uses: ./.github/actions/build 129 | - name: Act 130 | uses: ./ 131 | with: 132 | secret-ids: JsonSecret 133 | - name: Assert Default Is No Json Secrets 134 | run: npm run integration-test __integration_tests__/parse_json_secrets.test.ts 135 | 136 | af-south-1-integration-test: 137 | runs-on: ubuntu-latest 138 | needs: unit-tests 139 | steps: 140 | - name: Checkout 141 | uses: actions/checkout@v4 142 | - name: Build 143 | uses: ./.github/actions/build 144 | with: 145 | aws-region: af-south-1 146 | - name: Act 147 | uses: ./ 148 | with: 149 | secret-ids: JsonSecret 150 | auto-select-family-attempt-timeout: '2000' 151 | - name: Assert 152 | run: npm run integration-test __integration_tests__/parse_json_secrets.test.ts 153 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | coverage -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | README.md @ecraw-amzn 2 | *.ts @aws-actions/aws-secrets-manager-pr-br 3 | *.js @aws-actions/aws-secrets-manager-pr-br 4 | *.json @aws-actions/aws-secrets-manager-pr-br 5 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Use AWS Secrets Manager secrets in GitHub jobs 2 | 3 | To use a secret in a GitHub job, you can use a GitHub action to retrieve secrets from AWS Secrets Manager and add them as masked [Environment variables](https://docs.github.com/en/actions/learn-github-actions/environment-variables) in your GitHub workflow. For more information about GitHub Actions, see [Understanding GitHub Actions](https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions) in the *GitHub Docs*. 4 | 5 | When you add a secret to your GitHub environment, it is available to all other steps in your GitHub job. Follow the guidance in [Security hardening for GitHub Actions](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions) to help prevent secrets in your environment from being misused. 6 | 7 | You can set the entire string in the secret value as the environment variable value, or if the string is JSON, you can parse the JSON to set individual environment variables for each JSON key-value pair. If the secret value is a binary, the action converts it to a string. 8 | 9 | To view the environment variables created from your secrets, turn on debug logging. For more information, see [Enabling debug logging](https://docs.github.com/en/actions/monitoring-and-troubleshooting-workflows/enabling-debug-logging) in the *GitHub Docs*. 10 | 11 | To use the environment variables created from your secrets, see [Environment variables](https://docs.github.com/en/actions/learn-github-actions/environment-variables) in the *GitHub Docs*. 12 | 13 | ### Prerequisites 14 | 15 | To use this action, you first need to configure AWS credentials and set the AWS Region in your GitHub environment by using the `configure-aws-credentials` step. Follow the instructions in [Configure AWS Credentials Action For GitHub Actions](https://github.com/aws-actions/configure-aws-credentials) to **Assume role directly using GitHub OIDC provider**. This allows you to use short-lived credentials and avoid storing additional access keys outside of Secrets Manager. 16 | 17 | The IAM role the action assumes must have the following permissions: 18 | + `GetSecretValue` on the secrets you want to retrieve. 19 | + `ListSecrets` on all secrets. 20 | + \(Optional\) `Decrypt` on the KMS key if the secrets are encrypted with a customer managed key. 21 | 22 | For more information, see [Authentication and access control for AWS Secrets Manager](auth-and-access.md). 23 | 24 | ### Usage 25 | 26 | To use the action, add a step to your workflow that uses the following syntax. 27 | 28 | ``` 29 | - name: Step name 30 | uses: aws-actions/aws-secretsmanager-get-secrets@v2 31 | with: 32 | secret-ids: | 33 | secretId1 34 | ENV_VAR_NAME, secretId2 35 | name-transformation: (Optional) uppercase|lowercase|none 36 | parse-json-secrets: (Optional) true|false 37 | auto-select-family-attempt-timeout: (Optional) positive integer 38 | ``` 39 | Parameters 40 | 41 | - `secret-ids` Secret ARNS, names, and name prefixes. 42 | 43 | By default, the step creates each environment variable name from the secret name, transformed to include only uppercase letters, numbers, and underscores, and so that it doesn't begin with a number. 44 | 45 | To set the environment variable name, enter it before the secret ID, followed by a comma. For example `ENV_VAR_1, secretId` creates an environment variable named **ENV\_VAR\_1** from the secret `secretId`. 46 | 47 | The environment variable name can consist of uppercase letters, numbers, and underscores. 48 | 49 | To use a prefix, enter at least three characters followed by an asterisk. For example `dev*` matches all secrets with a name beginning in **dev**. The maximum number of matching secrets that can be retrieved is 100. If you set the variable name, and the prefix matches multiple secrets, then the action fails. 50 | 51 | - `name-transformation` 52 | 53 | By default, the step creates each environment variable name from the secret name, transformed to include only uppercase letters, numbers, and underscores, and so that it doesn't begin with a number. For the letters in the name, you can configure the step to use lowercase letters with `lowercase` or to not change the case of the letters with `none`. The default value is `uppercase`. 54 | 55 | - `parse-json-secrets` 56 | 57 | (Optional - default false) By default, the action sets the environment variable value to the entire JSON string in the secret value. 58 | 59 | Set `parse-json-secrets` to `true` to create environment variables for each key/value pair in the JSON. 60 | 61 | Note that if the JSON uses case-sensitive keys such as "name" and "Name", the action will have duplicate name conflicts. In this case, set `parse-json-secrets` to `false` and parse the JSON secret value separately. 62 | 63 | - `auto-select-family-attempt-timeout` 64 | 65 | (Optional - default 1000) Specifies the timeout (in milliseconds) for attempting to connect to the first IP address in a dual-stack DNS lookup. This setting is crucial especially when GitHub Action workers are geographically distant from the target region where the secrets are stored. The timeout must be greater than ot equal to 10 ms 66 | 67 | Set `auto-select-family-attempt-timeout` to any positive integer that is greater than or equal to 10 ms to set the timeout between each call to that value in milliseconds. 68 | ### Environment variable naming 69 | 70 | The environment variables created by the action are named the same as the secrets they come from. Environment variables have stricter naming requirements than secrets, so the action transforms secret names to meet those requirements. For example, the action transforms lowercase letters to uppercase letters. If you parse the JSON of the secret, then the environment variable name includes both the secret name and the JSON key name, for example `MYSECRET_KEYNAME`. 71 | 72 | If two environment variables would end up with the same name, the action fails. In this case, you must specify the names you want to use for the environment variables as *aliases*. 73 | 74 | Examples of when the names might conflict: 75 | + A secret named "MySecret" and a secret named "mysecret" would both become environment variables named "MYSECRET". 76 | + A secret named "Secret_keyname" and a JSON-parsed secret named "Secret" with a key named "keyname" would both become environment variables named "SECRET_KEYNAME". 77 | 78 | You can set the environment variable name by specifying an *alias*, as shown in the following example which creates a variable named `ENV_VAR_NAME`. 79 | 80 | ``` 81 | secret-ids: | 82 | ENV_VAR_NAME, secretId2 83 | ``` 84 | 85 | **Blank aliases** 86 | + If you set `parse-json-secrets: true` and enter a blank alias, followed by a comma and then the secret ID, the action names the environment variable the same as the parsed JSON keys. The variable names do not include the secret name. 87 | 88 | If the secret doesn't contain valid JSON, then the action creates one environment variable and names it the same as the secret name. 89 | + If you set `parse-json-secrets: false` and enter a blank alias, followed by a comma and the secret ID, the action names the environment variables as if you did not specify an alias. 90 | 91 | The following example shows a blank alias. 92 | 93 | ``` 94 | ,secret2 95 | ``` 96 | 97 | ### Examples 98 | 99 | **Example 1 Get secrets by name and by ARN** 100 | The following example creates environment variables for secrets identified by name and by ARN. 101 | 102 | ``` 103 | - name: Get secrets by name and by ARN 104 | uses: aws-actions/aws-secretsmanager-get-secrets@v2 105 | with: 106 | secret-ids: | 107 | exampleSecretName 108 | arn:aws:secretsmanager:us-east-2:123456789012:secret:test1-a1b2c3 109 | 0/test/secret 110 | /prod/example/secret 111 | SECRET_ALIAS_1,test/secret 112 | SECRET_ALIAS_2,arn:aws:secretsmanager:us-east-2:123456789012:secret:test2-a1b2c3 113 | ,secret2 114 | ``` 115 | Environment variables created: 116 | 117 | ``` 118 | EXAMPLESECRETNAME: secretValue1 119 | TEST1: secretValue2 120 | _0_TEST_SECRET: secretValue3 121 | _PROD_EXAMPLE_SECRET: secretValue4 122 | SECRET_ALIAS_1: secretValue5 123 | SECRET_ALIAS_2: secretValue6 124 | SECRET2: secretValue7 125 | ``` 126 | 127 | **Example 2 Get all secrets that begin with a prefix** 128 | The following example creates environment variables for all secrets with names that begin with *beta*. 129 | 130 | ``` 131 | - name: Get Secret Names by Prefix 132 | uses: aws-actions/aws-secretsmanager-get-secrets@v2 133 | with: 134 | secret-ids: | 135 | beta* # Retrieves all secrets that start with 'beta' 136 | ``` 137 | Environment variables created: 138 | 139 | ``` 140 | BETASECRETNAME: secretValue1 141 | BETATEST: secretValue2 142 | BETA_NEWSECRET: secretValue3 143 | ``` 144 | 145 | **Example 3 Parse JSON in secret** 146 | The following example creates environment variables by parsing the JSON in the secret. 147 | 148 | ``` 149 | - name: Get Secrets by Name and by ARN 150 | uses: aws-actions/aws-secretsmanager-get-secrets@v2 151 | with: 152 | secret-ids: | 153 | test/secret 154 | ,secret2 155 | parse-json-secrets: true 156 | ``` 157 | The secret `test/secret` has the following secret value. 158 | 159 | ``` 160 | { 161 | "api_user": "user", 162 | "api_key": "key", 163 | "config": { 164 | "active": "true" 165 | } 166 | } 167 | ``` 168 | The secret `secret2` has the following secret value. 169 | 170 | ``` 171 | { 172 | "myusername": "alejandro_rosalez", 173 | "mypassword": "EXAMPLE_PASSWORD" 174 | } 175 | ``` 176 | Environment variables created: 177 | 178 | ``` 179 | TEST_SECRET_API_USER: "user" 180 | TEST_SECRET_API_KEY: "key" 181 | TEST_SECRET_CONFIG_ACTIVE: "true" 182 | MYUSERNAME: "alejandro_rosalez" 183 | MYPASSWORD: "EXAMPLE_PASSWORD" 184 | ``` 185 | 186 | **Example 4 Use lowercase letters for environment variable names** 187 | The following example creates an environment variable with a lowercase name. 188 | 189 | ``` 190 | - name: Get secrets 191 | uses: aws-actions/aws-secretsmanager-get-secrets@v2 192 | with: 193 | secret-ids: exampleSecretName 194 | name-transformation: lowercase 195 | ``` 196 | 197 | Environment variable created: 198 | 199 | ``` 200 | examplesecretname: secretValue 201 | ``` 202 | 203 | **Example 5 Setting the timeout to 2 seconds** 204 | The following example sets the timeout between each call to be 2 seconds 205 | 206 | ``` 207 | - name: Get secrets with custom timeout 208 | uses: aws-actions/aws-secretsmanager-get-secrets@v2 209 | with: 210 | secret-ids: | 211 | test/secret 212 | prod/secret 213 | auto-select-family-attempt-timeout: 2000 # Sets timeout to 2 seconds between calls 214 | ``` 215 | 216 | Environment variables created: 217 | 218 | ``` 219 | TEST_SECRET: secretValue1 220 | PROD_SECRET: secretValue2 221 | ``` 222 | 223 | ## Security 224 | 225 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 226 | 227 | ## License 228 | 229 | This library is licensed under the MIT-0 License. See the LICENSE file. 230 | -------------------------------------------------------------------------------- /__integration_tests__/name_transformation.base.ts: -------------------------------------------------------------------------------- 1 | export function nameTransformationTest(transform: (secretName: string) => string) { 2 | const dataset = [ 3 | // Standard name qualified test 4 | ['SampleSecret1', 'SomeSampleSecret1'], 5 | // Special characters escaping test 6 | ['_special_chars_secret', 'SomeSampleSecret2'], 7 | // Secret starting with numerical character escape test 8 | ['_0_special_chars_secret', 'SomeSampleSecret3'], 9 | // Prefix matching test 10 | ['PrefixSecret1', 'PrefixSecret1Value'], 11 | ['PrefixSecret2', 'PrefixSecret2Value'], 12 | // Json value expansion 13 | ['JsonSecret_api_user', 'user'], 14 | ['JsonSecret_api_key', 'key'], 15 | ['JsonSecret_config_active', 'true'], 16 | // Alias test 17 | ['SampleSecret1_Alias', 'SomeSampleSecret1'] 18 | ].map(([secretName, expectedValue]) => [transform(secretName), expectedValue]); 19 | 20 | test.each(dataset)('Secret with name %s test', (secretName, expectedValue) => { 21 | const secretValue = process.env[secretName]; 22 | expect(secretValue).toBe(expectedValue); 23 | }); 24 | } -------------------------------------------------------------------------------- /__integration_tests__/name_transformation/lowercase.test.ts: -------------------------------------------------------------------------------- 1 | import { nameTransformationTest } from "../name_transformation.base"; 2 | 3 | describe('Lowercased Transformation Variables Assert', () => { 4 | nameTransformationTest(secretName => secretName.toLowerCase()); 5 | }); -------------------------------------------------------------------------------- /__integration_tests__/name_transformation/none.test.ts: -------------------------------------------------------------------------------- 1 | import { nameTransformationTest } from "../name_transformation.base"; 2 | 3 | describe('No Transformation Variables Assert', () => { 4 | nameTransformationTest(secretName => secretName); 5 | }); -------------------------------------------------------------------------------- /__integration_tests__/name_transformation/uppercase.test.ts: -------------------------------------------------------------------------------- 1 | import { nameTransformationTest } from "../name_transformation.base"; 2 | 3 | describe('Uppercased Transformation Variables Assert', () => { 4 | nameTransformationTest(secretName => secretName.toUpperCase()); 5 | }); -------------------------------------------------------------------------------- /__integration_tests__/parse_json_secrets.test.ts: -------------------------------------------------------------------------------- 1 | describe('parse-json-secrets: false Variables Assert', () => { 2 | it('Has secret name, does not have json keys ', () => { 3 | expect(process.env.JSONSECRET).not.toBeUndefined(); 4 | expect(process.env.JSONSECRET_API_USER).toBeUndefined(); 5 | expect(process.env.JSONSECRET_API_KEY).toBeUndefined(); 6 | expect(process.env.JSONSECRET_CONFIG_ACTIVE).toBeUndefined(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /__tests__/cleanup.test.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import { cleanup } from "../src/cleanup"; 3 | import { CLEANUP_NAME } from "../src/constants"; 4 | import * as utils from "../src/utils"; 5 | 6 | jest.mock('@actions/core'); 7 | 8 | const TEST_SECRET_VALUE = "secret"; 9 | const TEST_ENVIRONMENT = { 10 | SECRETS_LIST_CLEAN_UP: JSON.stringify(["TEST_SECRET", "TEST_SECRET_DB_HOST", "TEST_SECRET_API_KEY"]), 11 | TEST_SECRET: TEST_SECRET_VALUE, 12 | TEST_SECRET_DB_HOST: TEST_SECRET_VALUE, 13 | TEST_SECRET_API_KEY: TEST_SECRET_VALUE 14 | }; 15 | 16 | 17 | describe('Test post cleanup action', () => { 18 | const OLD_ENV = process.env; 19 | 20 | beforeEach(() => { 21 | jest.clearAllMocks(); 22 | process.env = {...OLD_ENV, ...TEST_ENVIRONMENT}; 23 | }); 24 | 25 | afterEach(() => { 26 | process.env = OLD_ENV; 27 | }); 28 | 29 | test ('Cleans a single variable from the environment', async () => { 30 | // Test that variable is present 31 | expect(process.env["TEST_SECRET"]).toEqual(TEST_SECRET_VALUE); 32 | 33 | utils.cleanVariable("TEST_SECRET"); 34 | 35 | // Test that variable is removed 36 | expect(core.exportVariable).toHaveBeenCalledWith('TEST_SECRET', ''); 37 | expect(process.env["TEST_SECRET"]).toBeUndefined(); 38 | }); 39 | 40 | test('Replaces AWS credential and region env vars with empty strings', async () => { 41 | await cleanup(); 42 | 43 | expect(core.setFailed).toHaveBeenCalledTimes(0); 44 | expect(core.exportVariable).toHaveBeenCalledTimes(4); 45 | 46 | expect(core.exportVariable).toHaveBeenCalledWith('TEST_SECRET', ''); 47 | expect(core.exportVariable).toHaveBeenCalledWith('TEST_SECRET_DB_HOST', ''); 48 | expect(core.exportVariable).toHaveBeenCalledWith('TEST_SECRET_API_KEY', ''); 49 | expect(core.exportVariable).toHaveBeenCalledWith(CLEANUP_NAME, ''); 50 | }); 51 | 52 | test ('Fails the action if a variable is still present after being cleaned', async () => { 53 | const utilSpy = jest.spyOn(utils, 'cleanVariable').mockImplementation(() => jest.fn()); 54 | await cleanup(); 55 | 56 | // Mocked cleaning did not remove the variable, so this should fail 57 | expect(core.setFailed).toHaveBeenCalledTimes(1); 58 | }); 59 | }); -------------------------------------------------------------------------------- /__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import { mockClient } from "aws-sdk-client-mock"; 3 | import { 4 | GetSecretValueCommand, ListSecretsCommand, 5 | SecretsManagerClient, 6 | } from '@aws-sdk/client-secrets-manager'; 7 | import { run } from "../src"; 8 | import { CLEANUP_NAME } from "../src/constants"; 9 | 10 | const DEFAULT_TEST_ENV = { 11 | AWS_DEFAULT_REGION: 'us-east-1' 12 | }; 13 | 14 | import * as net from 'net'; 15 | 16 | const smMockClient = mockClient(SecretsManagerClient); 17 | 18 | const TEST_NAME = "test/*"; 19 | 20 | const TEST_NAME_1 = "test/one"; 21 | const SECRET_1 = '{"user": "admin", "password": "adminpw"}'; 22 | 23 | const TEST_NAME_2 = "test/two"; 24 | const SECRET_2 = '{"user": "integ", "password": "integpw"}'; 25 | 26 | const TEST_NAME_3 = "app/secret"; 27 | const ENV_NAME_3 = "SECRET_ALIAS"; 28 | const SECRET_3 = "secretString1"; 29 | const TEST_INPUT_3 = ENV_NAME_3 + "," + TEST_NAME_3; 30 | 31 | const TEST_ARN_1 = 'arn:aws:secretsmanager:ap-south-1:123456789000:secret:test2-aBcdef'; 32 | const TEST_NAME_4 = 'arn/secret-name'; 33 | const ENV_NAME_4 = 'ARN_ALIAS'; 34 | const SECRET_4 = "secretString2"; 35 | const TEST_ARN_INPUT = ENV_NAME_4 + "," + TEST_ARN_1; 36 | 37 | const BLANK_NAME = "test/blank"; 38 | const SECRET_FOR_BLANK = '{"username": "integ", "password": "integpw", "config": {"id1": "example1"}}'; 39 | const BLANK_ALIAS_INPUT = "," + BLANK_NAME; 40 | 41 | const BLANK_NAME_2 = "test/blank2"; 42 | const SECRET_FOR_BLANK_2 = "blankNameSecretString"; 43 | const BLANK_ALIAS_INPUT_2 = "," + BLANK_NAME_2; 44 | 45 | const BLANK_NAME_3 = "test/blank3"; 46 | const SECRET_FOR_BLANK_3 = '{"username": "integ", "password": "integpw", "config": {"id2": "example2"}}'; 47 | const BLANK_ALIAS_INPUT_3 = "," + BLANK_NAME_3; 48 | 49 | 50 | 51 | const VALID_TIMEOUT = '3000'; 52 | const INVALID_TIMEOUT_STRING = 'abc'; 53 | const DEFAULT_TIMEOUT = '1000'; 54 | const INVALID_TIMEOUT = '9'; 55 | 56 | // Mock the inputs for Github action 57 | jest.mock('@actions/core', () => { 58 | return { 59 | getMultilineInput: jest.fn(), 60 | getBooleanInput: jest.fn(), 61 | getInput: jest.fn(), 62 | setFailed: jest.fn(), 63 | info: jest.fn(), 64 | debug: jest.fn(), 65 | exportVariable: jest.fn((name: string, val: string) => process.env[name] = val), 66 | setSecret: jest.fn(), 67 | }; 68 | }); 69 | 70 | jest.mock('net', () => { 71 | return { 72 | setDefaultAutoSelectFamilyAttemptTimeout: jest.fn() 73 | } 74 | }); 75 | 76 | describe('Test main action', () => { 77 | const OLD_ENV = process.env; 78 | 79 | beforeEach(() => { 80 | jest.clearAllMocks(); 81 | smMockClient.reset(); 82 | process.env = {...OLD_ENV, ...DEFAULT_TEST_ENV}; 83 | }); 84 | 85 | afterEach(() => { 86 | process.env = OLD_ENV; 87 | }); 88 | 89 | test('Retrieves and sets the requested secrets as environment variables, parsing JSON', async () => { 90 | const getInputSpy = jest.spyOn(core, 'getInput'); 91 | getInputSpy.mockImplementation((name) => { 92 | switch(name) { 93 | case 'auto-select-family-attempt-timeout': 94 | return DEFAULT_TIMEOUT; 95 | case 'name-transformation': 96 | return 'uppercase'; 97 | default: 98 | return ''; 99 | } 100 | }); 101 | const booleanSpy = jest.spyOn(core, "getBooleanInput").mockReturnValue(true); 102 | const multilineInputSpy = jest.spyOn(core, "getMultilineInput").mockReturnValue( 103 | [TEST_NAME, TEST_INPUT_3, TEST_ARN_INPUT, BLANK_ALIAS_INPUT] 104 | ); 105 | 106 | 107 | // Mock all Secrets Manager calls 108 | smMockClient 109 | .on(GetSecretValueCommand, { SecretId: TEST_NAME_1}) 110 | .resolves({ Name: TEST_NAME_1, SecretString: SECRET_1 }) 111 | .on(GetSecretValueCommand, {SecretId: TEST_NAME_2 }) 112 | .resolves({ Name: TEST_NAME_2, SecretString: SECRET_2 }) 113 | .on(GetSecretValueCommand, { SecretId: TEST_NAME_3 }) 114 | .resolves({ Name: TEST_NAME_3, SecretString: SECRET_3 }) 115 | .on(GetSecretValueCommand, { // Retrieve arn secret 116 | SecretId: TEST_ARN_1, 117 | }) 118 | .resolves({ 119 | Name: TEST_NAME_4, 120 | SecretString: SECRET_4 121 | }) 122 | .on(ListSecretsCommand) 123 | .resolves({ 124 | SecretList: [ 125 | { 126 | Name: TEST_NAME_1 127 | }, 128 | { 129 | Name: TEST_NAME_2 130 | } 131 | ] 132 | }) 133 | .on(GetSecretValueCommand, { SecretId: BLANK_NAME }) 134 | .resolves({ Name: BLANK_NAME, SecretString: SECRET_FOR_BLANK }); 135 | 136 | await run(); 137 | expect(core.setFailed).not.toHaveBeenCalled(); 138 | expect(core.exportVariable).toHaveBeenCalledTimes(10); 139 | 140 | // JSON secrets should be parsed 141 | expect(core.exportVariable).toHaveBeenCalledWith('TEST_ONE_USER', 'admin'); 142 | expect(core.exportVariable).toHaveBeenCalledWith('TEST_ONE_PASSWORD', 'adminpw'); 143 | expect(core.exportVariable).toHaveBeenCalledWith('TEST_TWO_USER', 'integ'); 144 | expect(core.exportVariable).toHaveBeenCalledWith('TEST_TWO_PASSWORD', 'integpw'); 145 | 146 | expect(core.exportVariable).toHaveBeenCalledWith(ENV_NAME_3, SECRET_3); 147 | expect(core.exportVariable).toHaveBeenCalledWith(ENV_NAME_4, SECRET_4); 148 | 149 | // Case when alias is blank, but still comma delimited in workflow and json is parsed 150 | // ex: ,test5/secret 151 | expect(core.exportVariable).toHaveBeenCalledWith("USERNAME", "integ"); 152 | expect(core.exportVariable).toHaveBeenCalledWith("PASSWORD", "integpw"); 153 | expect(core.exportVariable).toHaveBeenCalledWith("CONFIG_ID1", "example1"); 154 | 155 | expect(core.exportVariable).toHaveBeenCalledWith( 156 | CLEANUP_NAME, 157 | JSON.stringify([ 158 | 'TEST_ONE_USER', 'TEST_ONE_PASSWORD', 159 | 'TEST_TWO_USER', 'TEST_TWO_PASSWORD', 160 | ENV_NAME_3, 161 | ENV_NAME_4, 162 | "USERNAME", "PASSWORD", "CONFIG_ID1" 163 | ]) 164 | ); 165 | 166 | booleanSpy.mockClear(); 167 | multilineInputSpy.mockClear(); 168 | }); 169 | 170 | test('Defaults to correct behavior with empty string alias', async () => { 171 | const booleanSpy = jest.spyOn(core, "getBooleanInput").mockReturnValue(false); 172 | const multilineInputSpy = jest.spyOn(core, "getMultilineInput").mockReturnValue( 173 | [BLANK_ALIAS_INPUT_2, BLANK_ALIAS_INPUT_3] 174 | ); 175 | 176 | smMockClient 177 | .on(GetSecretValueCommand, { SecretId: BLANK_NAME_2 }) 178 | .resolves({ Name: BLANK_NAME_2, SecretString: SECRET_FOR_BLANK_2 }) 179 | .on(GetSecretValueCommand, { SecretId: BLANK_NAME_3 }) 180 | .resolves({ Name: BLANK_NAME_3, SecretString: SECRET_FOR_BLANK_3 }); 181 | 182 | await run(); 183 | expect(core.setFailed).not.toHaveBeenCalled(); 184 | expect(core.exportVariable).toHaveBeenCalledTimes(3); 185 | 186 | // Case when alias is blank, but still comma delimited in workflow and no json is parsed 187 | // ex: ,test/blank2 188 | expect(core.exportVariable).toHaveBeenCalledWith("TEST_BLANK2", "blankNameSecretString"); 189 | expect(core.exportVariable).toHaveBeenCalledWith("TEST_BLANK3", '{"username": "integ", "password": "integpw", "config": {"id2": "example2"}}'); 190 | 191 | expect(core.exportVariable).toHaveBeenCalledWith( 192 | CLEANUP_NAME, 193 | JSON.stringify([ 194 | "TEST_BLANK2", 195 | "TEST_BLANK3" 196 | ]) 197 | ); 198 | 199 | booleanSpy.mockClear(); 200 | multilineInputSpy.mockClear(); 201 | }); 202 | 203 | test('Fails the action when an error occurs in Secrets Manager', async () => { 204 | const booleanSpy = jest.spyOn(core, "getBooleanInput").mockReturnValue(true); 205 | const multilineInputSpy = jest.spyOn(core, "getMultilineInput").mockReturnValue( 206 | [TEST_NAME, TEST_INPUT_3, TEST_ARN_INPUT] 207 | ); 208 | 209 | smMockClient.onAnyCommand().resolves({}); 210 | 211 | await run(); 212 | expect(core.setFailed).toHaveBeenCalledTimes(1); 213 | 214 | booleanSpy.mockClear(); 215 | multilineInputSpy.mockClear(); 216 | }); 217 | 218 | test('Fails the action when multiple secrets exported the same variable name', async () => { 219 | const booleanSpy = jest.spyOn(core, "getBooleanInput").mockReturnValue(true); 220 | const multilineInputSpy = jest.spyOn(core, "getMultilineInput").mockReturnValue( 221 | [TEST_NAME, TEST_INPUT_3, TEST_ARN_INPUT] 222 | ); 223 | const nameTransformationSpy = jest.spyOn(core, 'getInput').mockReturnValue('uppercase'); 224 | 225 | smMockClient 226 | .on(GetSecretValueCommand, { SecretId: TEST_NAME_1}) 227 | .resolves({ Name: TEST_NAME_1, SecretString: SECRET_1 }) 228 | .on(GetSecretValueCommand, {SecretId: TEST_NAME_2 }) 229 | .resolves({ Name: TEST_NAME_2, SecretString: SECRET_2 }) 230 | .on(GetSecretValueCommand, { SecretId: TEST_NAME_3 }) 231 | .resolves({ Name: TEST_NAME_3, SecretString: SECRET_3 }) 232 | .on(GetSecretValueCommand, { // Retrieve arn secret 233 | SecretId: TEST_ARN_1, 234 | }) 235 | .resolves({ 236 | Name: TEST_NAME_4, 237 | SecretString: SECRET_4 238 | }) 239 | .on(GetSecretValueCommand) // default 240 | .resolves({Name: "DefaultName", SecretString: "Default"}) 241 | .on(ListSecretsCommand) 242 | .resolves({ 243 | SecretList: [ 244 | { 245 | Name: "TEST/SECRET/2" 246 | }, 247 | { 248 | Name: "TEST/SECRET@2" 249 | } 250 | ] 251 | }); 252 | 253 | await run(); 254 | expect(core.setFailed).toHaveBeenCalledTimes(1); 255 | 256 | booleanSpy.mockClear(); 257 | multilineInputSpy.mockClear(); 258 | nameTransformationSpy.mockClear(); 259 | }); 260 | 261 | 262 | test('Keep existing cleanup list', async() => { 263 | // Set existing cleanup list 264 | process.env = {...process.env, SECRETS_LIST_CLEAN_UP: JSON.stringify(["EXISTING_TEST_SECRET", "EXISTING_TEST_SECRET_DB_HOST"])}; 265 | 266 | const getInputSpy = jest.spyOn(core, 'getInput'); 267 | getInputSpy.mockImplementation((name) => { 268 | switch(name) { 269 | case 'auto-select-family-attempt-timeout': 270 | return DEFAULT_TIMEOUT; 271 | case 'name-transformation': 272 | return 'uppercase'; 273 | default: 274 | return ''; 275 | } 276 | }); 277 | 278 | const booleanSpy = jest.spyOn(core, "getBooleanInput").mockReturnValue(true); 279 | const multilineInputSpy = jest.spyOn(core, "getMultilineInput").mockReturnValue( 280 | [TEST_NAME, TEST_INPUT_3, TEST_ARN_INPUT, BLANK_ALIAS_INPUT] 281 | ); 282 | 283 | 284 | // Mock all Secrets Manager calls 285 | smMockClient 286 | .on(GetSecretValueCommand, { SecretId: TEST_NAME_1}) 287 | .resolves({ Name: TEST_NAME_1, SecretString: SECRET_1 }) 288 | .on(GetSecretValueCommand, {SecretId: TEST_NAME_2 }) 289 | .resolves({ Name: TEST_NAME_2, SecretString: SECRET_2 }) 290 | .on(GetSecretValueCommand, { SecretId: TEST_NAME_3 }) 291 | .resolves({ Name: TEST_NAME_3, SecretString: SECRET_3 }) 292 | .on(GetSecretValueCommand, { // Retrieve arn secret 293 | SecretId: TEST_ARN_1, 294 | }) 295 | .resolves({ 296 | Name: TEST_NAME_4, 297 | SecretString: SECRET_4 298 | }) 299 | .on(ListSecretsCommand) 300 | .resolves({ 301 | SecretList: [ 302 | { 303 | Name: TEST_NAME_1 304 | }, 305 | { 306 | Name: TEST_NAME_2 307 | } 308 | ] 309 | }) 310 | .on(GetSecretValueCommand, { SecretId: BLANK_NAME }) 311 | .resolves({ Name: BLANK_NAME, SecretString: SECRET_FOR_BLANK }); 312 | 313 | await run(); 314 | expect(core.setFailed).not.toHaveBeenCalled(); 315 | expect(core.exportVariable).toHaveBeenCalledTimes(10); 316 | 317 | // JSON secrets should be parsed 318 | expect(core.exportVariable).toHaveBeenCalledWith('TEST_ONE_USER', 'admin'); 319 | expect(core.exportVariable).toHaveBeenCalledWith('TEST_ONE_PASSWORD', 'adminpw'); 320 | expect(core.exportVariable).toHaveBeenCalledWith('TEST_TWO_USER', 'integ'); 321 | expect(core.exportVariable).toHaveBeenCalledWith('TEST_TWO_PASSWORD', 'integpw'); 322 | 323 | expect(core.exportVariable).toHaveBeenCalledWith(ENV_NAME_3, SECRET_3); 324 | expect(core.exportVariable).toHaveBeenCalledWith(ENV_NAME_4, SECRET_4); 325 | 326 | // Case when alias is blank, but still comma delimited in workflow and json is parsed 327 | // ex: ,test5/secret 328 | expect(core.exportVariable).toHaveBeenCalledWith("USERNAME", "integ"); 329 | expect(core.exportVariable).toHaveBeenCalledWith("PASSWORD", "integpw"); 330 | expect(core.exportVariable).toHaveBeenCalledWith("CONFIG_ID1", "example1"); 331 | 332 | expect(core.exportVariable).toHaveBeenCalledWith( 333 | CLEANUP_NAME, 334 | JSON.stringify([ 335 | 'EXISTING_TEST_SECRET', 'EXISTING_TEST_SECRET_DB_HOST', 336 | 'TEST_ONE_USER', 'TEST_ONE_PASSWORD', 337 | 'TEST_TWO_USER', 'TEST_TWO_PASSWORD', 338 | ENV_NAME_3, 339 | ENV_NAME_4, 340 | "USERNAME", "PASSWORD", "CONFIG_ID1" 341 | ]) 342 | ); 343 | 344 | booleanSpy.mockClear(); 345 | multilineInputSpy.mockClear(); 346 | getInputSpy.mockClear(); 347 | }) 348 | 349 | test('handles invalid timeout string', async () => { 350 | const timeoutSpy = jest.spyOn(core, 'getInput').mockReturnValue(INVALID_TIMEOUT_STRING); 351 | 352 | smMockClient 353 | .on(GetSecretValueCommand) 354 | .resolves({ SecretString: 'test' }); 355 | 356 | await run(); 357 | 358 | expect(core.setFailed).toHaveBeenCalled(); 359 | 360 | 361 | timeoutSpy.mockClear(); 362 | 363 | }); 364 | 365 | test('handles valid timeout value', async () => { 366 | const timeoutSpy = jest.spyOn(core, 'getInput').mockReturnValue(VALID_TIMEOUT); 367 | 368 | smMockClient 369 | .on(GetSecretValueCommand) 370 | .resolves({ SecretString: 'test' }); 371 | 372 | await run(); 373 | 374 | expect(net.setDefaultAutoSelectFamilyAttemptTimeout).toHaveBeenCalledWith(3000); 375 | 376 | 377 | timeoutSpy.mockClear(); 378 | }); 379 | 380 | 381 | test('handles invalid timeout value', async () => { 382 | const timeoutSpy = jest.spyOn(core, 'getInput').mockReturnValue(INVALID_TIMEOUT); 383 | 384 | smMockClient 385 | .on(GetSecretValueCommand) 386 | .resolves({ SecretString: 'test' }); 387 | 388 | await run(); 389 | 390 | expect(core.setFailed).toHaveBeenCalled(); 391 | 392 | 393 | timeoutSpy.mockClear(); 394 | }) 395 | 396 | }); -------------------------------------------------------------------------------- /__tests__/utils.test.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import { mockClient } from "aws-sdk-client-mock"; 3 | import { 4 | GetSecretValueCommand, 5 | ListSecretsCommand, 6 | SecretsManagerClient, 7 | ResourceNotFoundException, ListSecretsCommandInput 8 | } from '@aws-sdk/client-secrets-manager'; 9 | import { 10 | buildSecretsList, 11 | getSecretValue, 12 | getSecretsWithPrefix, 13 | isJSONString, 14 | injectSecret, 15 | isSecretArn, 16 | extractAliasAndSecretIdFromInput, 17 | transformToValidEnvName, 18 | parseTransformationFunction, 19 | TransformationFunc 20 | } from "../src/utils"; 21 | 22 | import { CLEANUP_NAME, LIST_SECRETS_MAX_RESULTS } from "../src/constants"; 23 | 24 | const TEST_NAME = 'test/secret'; 25 | const TEST_ENV_NAME = 'TEST_SECRET'; 26 | const TEST_VALUE = 'test!secret!value!'; 27 | const SIMPLE_JSON_SECRET = '{"api_key": "testkey", "user": "testuser"}'; 28 | const NESTED_JSON_SECRET = '{"host":"127.0.0.1", "port": "3600", "config":{"db_user":"testuser","db_password":"testpw","options":{"a":"YES","b":"NO", "c": 100 }}}'; 29 | 30 | const VALID_ARN_1 = 'arn:aws:secretsmanager:us-east-1:123456789000:secret:test1-aBcdef'; 31 | const TEST_NAME_1 = 'test/secret1'; 32 | 33 | const VALID_ARN_2 = 'arn:aws:secretsmanager:ap-south-1:123456789000:secret:test2-aBcdef'; 34 | const TEST_NAME_2 = 'test/secret2'; 35 | 36 | const VALID_ARN_3 = 'arn:aws:secretsmanager:ap-south-1:123456789000:secret:test3-aBcdef'; 37 | 38 | const INVALID_ARN = 'aws:secretsmanager:us-east-1:123456789000:secret:test3-aBcdef'; 39 | 40 | jest.mock('@actions/core'); 41 | 42 | const smClient = new SecretsManagerClient({}); // Cannot send mock directly because of type enforcement 43 | const smMockClient = mockClient(smClient); 44 | 45 | 46 | describe('Test secret value retrieval', () => { 47 | beforeEach(() => { 48 | smMockClient.reset(); 49 | jest.clearAllMocks(); 50 | }); 51 | 52 | test('Retrieves a secret string', async () => { 53 | smMockClient.on(GetSecretValueCommand).resolves({ 54 | Name: TEST_NAME, 55 | SecretString: TEST_VALUE, 56 | }); 57 | 58 | const secretValue = await getSecretValue(smClient, TEST_NAME); 59 | expect(secretValue.secretValue).toStrictEqual(TEST_VALUE); 60 | }); 61 | 62 | test('Retrieves a secret string and returns with name if requested', async () => { 63 | smMockClient.on(GetSecretValueCommand).resolvesOnce({ 64 | Name: TEST_NAME_1, 65 | SecretString: SIMPLE_JSON_SECRET 66 | }).resolves({ 67 | SecretString: TEST_VALUE 68 | }); 69 | 70 | const secretValue1 = await getSecretValue(smClient, VALID_ARN_1); 71 | expect(secretValue1.name).toStrictEqual(TEST_NAME_1); 72 | expect(secretValue1.secretValue).toStrictEqual(SIMPLE_JSON_SECRET); 73 | 74 | // Throw an error if something wrong with secret name 75 | await expect(getSecretValue(smClient, TEST_NAME_2)).rejects.toThrow(); 76 | }); 77 | 78 | test('Retrieves a binary secret', async () => { 79 | const bytes = new TextEncoder().encode(TEST_VALUE); 80 | 81 | smMockClient.on(GetSecretValueCommand).resolves({ 82 | Name: TEST_NAME, 83 | SecretBinary: bytes, 84 | }); 85 | 86 | const secretValue = await getSecretValue(smClient, TEST_NAME); 87 | expect(secretValue.secretValue).toStrictEqual(TEST_VALUE); 88 | }); 89 | 90 | test('Throws an error if unable to retrieve the secret', async () => { 91 | const error = new ResourceNotFoundException({$metadata: {}, message: 'Error'}); 92 | smMockClient.on(GetSecretValueCommand).rejects(error); 93 | await expect(getSecretValue(smClient, TEST_NAME)).rejects.toThrow(error); 94 | }); 95 | 96 | test('Throws an error if the secret value is invalid', async () => { 97 | smMockClient.on(GetSecretValueCommand).resolves({}); 98 | await expect(getSecretValue(smClient, TEST_NAME)).rejects.toThrow(); 99 | }); 100 | 101 | test('Throws error on invalid list secrets response ', async () => { 102 | smMockClient 103 | .on(ListSecretsCommand) 104 | .resolves({}); 105 | await expect(getSecretsWithPrefix(smClient, "test", false)).rejects.toThrow(); 106 | }); 107 | 108 | test('Builds a complete list of secrets from user input', async () => { 109 | const input = ["test/*", "alternativeSecret"]; 110 | const expectedParams = { 111 | Filters: [ 112 | { 113 | Key: "name", 114 | Values: [ 115 | "test/", 116 | ] 117 | }, 118 | ], 119 | MaxResults: LIST_SECRETS_MAX_RESULTS, 120 | } as ListSecretsCommandInput; 121 | 122 | smMockClient.on(ListSecretsCommand).resolves({ 123 | SecretList: [ 124 | { 125 | ARN: VALID_ARN_1, 126 | Name: TEST_NAME_1 127 | }, 128 | { 129 | ARN: VALID_ARN_2, 130 | Name: TEST_NAME_2 131 | } 132 | ] 133 | }); 134 | const result = await buildSecretsList(smClient, input); 135 | expect(smMockClient).toHaveReceivedCommandTimes(ListSecretsCommand, 1); 136 | expect(smMockClient).toHaveReceivedCommandWith(ListSecretsCommand, expectedParams); 137 | expect(result).toEqual([TEST_NAME_1, TEST_NAME_2, 'alternativeSecret']); 138 | }); 139 | 140 | test('Builds a complete list of secrets, including alias, for prefix secret', async () => { 141 | const input = ["SECRET_ALIAS,test/*", "alternativeSecret"]; 142 | const expectedParams = { 143 | Filters: [ 144 | { 145 | Key: "name", 146 | Values: [ 147 | "test/", 148 | ] 149 | }, 150 | ], 151 | MaxResults: LIST_SECRETS_MAX_RESULTS, 152 | } as ListSecretsCommandInput; 153 | 154 | smMockClient.on(ListSecretsCommand).resolves({ 155 | SecretList: [ 156 | { 157 | ARN: VALID_ARN_1, 158 | Name: TEST_NAME_1 159 | } 160 | ] 161 | }); 162 | const result = await buildSecretsList(smClient, input); 163 | expect(smMockClient).toHaveReceivedCommandTimes(ListSecretsCommand, 1); 164 | expect(smMockClient).toHaveReceivedCommandWith(ListSecretsCommand, expectedParams); 165 | expect(result).toEqual(['SECRET_ALIAS,' + TEST_NAME_1, 'alternativeSecret']); 166 | }); 167 | 168 | 169 | test('Throws an error if a prefix filter is invalid or not specific enough', async () => { 170 | let input = ["/*", "alternativeSecret"]; 171 | await expect(buildSecretsList(smClient, input)).rejects.toThrow(); 172 | 173 | input = ["*not/a/prefix", "alternativeSecret"]; 174 | await expect(buildSecretsList(smClient, input)).rejects.toThrow(); 175 | 176 | input = ["a*", "alternativeSecret"]; 177 | await expect(buildSecretsList(smClient, input)).rejects.toThrow(); 178 | }); 179 | 180 | test('Throws an error if a prefix filter returns too many results', async () => { 181 | const input = ["too/many/matches/*"]; 182 | const expectedParams = { 183 | Filters: [ 184 | { 185 | Key: "name", 186 | Values: [ 187 | "too/many/matches/", 188 | ] 189 | }, 190 | ], 191 | MaxResults: LIST_SECRETS_MAX_RESULTS, 192 | } as ListSecretsCommandInput; 193 | 194 | smMockClient.on(ListSecretsCommand).resolves({ 195 | SecretList: [ 196 | { 197 | ARN: VALID_ARN_1, 198 | Name: TEST_NAME_1 199 | }, 200 | { 201 | ARN: VALID_ARN_2, 202 | Name: TEST_NAME_2 203 | } 204 | ], 205 | NextToken: "ThereAreTooManyResults" 206 | }); 207 | 208 | await expect(buildSecretsList(smClient, input)).rejects.toThrow(); 209 | expect(smMockClient).toHaveReceivedCommandTimes(ListSecretsCommand, 1); 210 | expect(smMockClient).toHaveReceivedCommandWith(ListSecretsCommand, expectedParams); 211 | }); 212 | 213 | test('Throws an error if a prefix filter has no results', async () => { 214 | const input = ["no/matches/*"]; 215 | const expectedParams = { 216 | Filters: [ 217 | { 218 | Key: "name", 219 | Values: [ 220 | "no/matches/", 221 | ] 222 | }, 223 | ], 224 | MaxResults: LIST_SECRETS_MAX_RESULTS, 225 | } as ListSecretsCommandInput; 226 | 227 | smMockClient.on(ListSecretsCommand).resolves({ 228 | SecretList: [] 229 | }); 230 | 231 | await expect(buildSecretsList(smClient, input)).rejects.toThrow(); 232 | expect(smMockClient).toHaveReceivedCommandTimes(ListSecretsCommand, 1); 233 | expect(smMockClient).toHaveReceivedCommandWith(ListSecretsCommand, expectedParams); 234 | }); 235 | 236 | test('Throws an error if a prefix filter with an alias returns more than 1 result', async () => { 237 | const input = ["SECRET_ALIAS,test/*"]; 238 | const expectedParams = { 239 | Filters: [ 240 | { 241 | Key: "name", 242 | Values: [ 243 | "test/", 244 | ] 245 | }, 246 | ], 247 | MaxResults: LIST_SECRETS_MAX_RESULTS, 248 | } as ListSecretsCommandInput; 249 | 250 | smMockClient.on(ListSecretsCommand).resolves({ 251 | SecretList: [ 252 | { 253 | ARN: VALID_ARN_1, 254 | Name: TEST_NAME_1 255 | }, 256 | { 257 | ARN: VALID_ARN_2, 258 | Name: TEST_NAME_2 259 | } 260 | ] 261 | }); 262 | 263 | await expect(buildSecretsList(smClient, input)).rejects.toThrow(); 264 | expect(smMockClient).toHaveReceivedCommandTimes(ListSecretsCommand, 1); 265 | expect(smMockClient).toHaveReceivedCommandWith(ListSecretsCommand, expectedParams); 266 | }); 267 | 268 | }); 269 | 270 | describe('Test secret parsing and handling', () => { 271 | beforeEach(() => { 272 | jest.clearAllMocks(); 273 | }); 274 | 275 | /* 276 | * Test: isSecretArn() 277 | */ 278 | test('Returns true for valid arn', () => { 279 | expect(isSecretArn(VALID_ARN_1)).toEqual(true); 280 | expect(isSecretArn(VALID_ARN_2)).toEqual(true); 281 | }); 282 | 283 | test('Return false for invalid arn or secret name', () => { 284 | expect(isSecretArn(INVALID_ARN)).toEqual(false); 285 | expect(isSecretArn(TEST_NAME)).toEqual(false); 286 | }); 287 | 288 | /* 289 | * Test: injectSecret() 290 | */ 291 | test('Stores a simple secret', () => { 292 | injectSecret(TEST_NAME, TEST_VALUE, false); 293 | expect(core.exportVariable).toHaveBeenCalledTimes(1); 294 | expect(core.exportVariable).toHaveBeenCalledWith(TEST_ENV_NAME, TEST_VALUE); 295 | }); 296 | 297 | test('Stores a JSON secret as string when parseJson is false', () => { 298 | injectSecret(TEST_NAME, SIMPLE_JSON_SECRET, false); 299 | expect(core.exportVariable).toHaveBeenCalledTimes(1); 300 | expect(core.exportVariable).toHaveBeenCalledWith(TEST_ENV_NAME, SIMPLE_JSON_SECRET); 301 | }); 302 | 303 | test('Throws an error if reserved name is used', () => { 304 | expect(() => { 305 | injectSecret(CLEANUP_NAME, TEST_VALUE, false); 306 | }).toThrow(); 307 | }); 308 | 309 | test('Stores a variable for each JSON key value when parseJson is true', () => { 310 | injectSecret(TEST_NAME, SIMPLE_JSON_SECRET, true); 311 | expect(core.exportVariable).toHaveBeenCalledTimes(2); 312 | expect(core.exportVariable).toHaveBeenCalledWith('TEST_SECRET_API_KEY', 'testkey'); 313 | expect(core.exportVariable).toHaveBeenCalledWith('TEST_SECRET_USER', 'testuser'); 314 | }); 315 | 316 | test('Stores a variable for nested JSON key values when parseJson is true', () => { 317 | injectSecret(TEST_NAME, NESTED_JSON_SECRET, true); 318 | expect(core.setSecret).toHaveBeenCalledTimes(7); 319 | expect(core.exportVariable).toHaveBeenCalledWith('TEST_SECRET_HOST', '127.0.0.1'); 320 | expect(core.exportVariable).toHaveBeenCalledWith('TEST_SECRET_PORT', '3600'); 321 | expect(core.exportVariable).toHaveBeenCalledWith('TEST_SECRET_CONFIG_DB_USER', 'testuser'); 322 | expect(core.exportVariable).toHaveBeenCalledWith('TEST_SECRET_CONFIG_DB_PASSWORD', 'testpw'); 323 | expect(core.exportVariable).toHaveBeenCalledWith('TEST_SECRET_CONFIG_OPTIONS_A', 'YES'); 324 | expect(core.exportVariable).toHaveBeenCalledWith('TEST_SECRET_CONFIG_OPTIONS_B', 'NO'); 325 | expect(core.exportVariable).toHaveBeenCalledWith('TEST_SECRET_CONFIG_OPTIONS_C', '100'); 326 | }); 327 | 328 | test('Maintains single underscore between prefix and numeric properties', () => { 329 | const secretName = 'DB'; 330 | const secretValue = JSON.stringify({ 331 | "7Value": "test-value" 332 | }); 333 | 334 | const secretsToCleanup = injectSecret( 335 | secretName, 336 | secretValue, 337 | true, 338 | undefined 339 | ); 340 | 341 | expect(secretsToCleanup).toHaveLength(1); 342 | expect(secretsToCleanup[0]).toBe('DB_7VALUE'); 343 | }); 344 | 345 | test('Maintains single underscore between prefix and numeric properties with a EnvName', () => { 346 | const secretName = 'DB'; 347 | const secretValue = JSON.stringify({ 348 | "7Value": "test-value" 349 | }); 350 | 351 | const secretsToCleanup = injectSecret( 352 | secretName, 353 | secretValue, 354 | true, 355 | undefined, 356 | TEST_ENV_NAME 357 | ); 358 | 359 | expect(secretsToCleanup).toHaveLength(1); 360 | expect(secretsToCleanup[0]).toBe('TEST_SECRET_7VALUE'); 361 | }); 362 | 363 | /* 364 | * Test: parseAliasFromId() 365 | */ 366 | test('Separates an alias from an id if provided', () => { 367 | // Expect whitespace to be cleaned 368 | expect(extractAliasAndSecretIdFromInput("SECRET_ALIAS, test/secret")).toEqual(['SECRET_ALIAS', 'test/secret']); 369 | expect(extractAliasAndSecretIdFromInput(`ARN_ALIAS,${VALID_ARN_1}`)).toEqual(['ARN_ALIAS', VALID_ARN_1]); 370 | }); 371 | 372 | test('Returns undefined for alias if none is provided', () => { 373 | expect(extractAliasAndSecretIdFromInput("test/secret")).toEqual([undefined, 'test/secret']); 374 | expect(extractAliasAndSecretIdFromInput(VALID_ARN_1)).toEqual([undefined, VALID_ARN_1]); 375 | }); 376 | 377 | test('Returns empty string for alias if none is provided, but comma delimited', () => { 378 | expect(extractAliasAndSecretIdFromInput(" , test/secret")).toEqual(['', 'test/secret']); 379 | expect(extractAliasAndSecretIdFromInput(" , "+VALID_ARN_3)).toEqual(['', VALID_ARN_3]); 380 | }); 381 | 382 | test('Throws an error if the provided alias cannot be used as the environment name', () => { 383 | expect(() => { 384 | extractAliasAndSecretIdFromInput("Invalid-env, test/secret") 385 | }).toThrow(); 386 | 387 | expect(() => { 388 | extractAliasAndSecretIdFromInput("0INVALID, test/secret") 389 | }).toThrow(); 390 | 391 | expect(() => { 392 | extractAliasAndSecretIdFromInput("@Invalid, test/secret") 393 | }).toThrow(); 394 | }); 395 | 396 | /* 397 | * Test: transformToValidEnvName() 398 | */ 399 | test('Prevents illegal special characters in environment name', () => { 400 | expect(transformToValidEnvName('prod/db/admin')).toBe('PROD_DB_ADMIN') 401 | }); 402 | 403 | test('Prevents leading digits in environment name', () => { 404 | expect(transformToValidEnvName('0Admin')).toBe('_0ADMIN') 405 | }); 406 | 407 | 408 | test('Transformation function is applied', () => { 409 | expect(transformToValidEnvName('secret3', (x) => x.toUpperCase())).toBe('SECRET3') 410 | }); 411 | 412 | /* 413 | * Test: isJSONString() 414 | */ 415 | test('Test invalid JSON "100" ', () => { 416 | expect(isJSONString('100')).toBe(false) 417 | }); 418 | 419 | test('Test invalid JSON key { a: "100" } ', () => { 420 | expect(isJSONString('{ a: "100" }')).toBe(false) 421 | }); 422 | 423 | test('Test invalid array ["a", "b"] ', () => { 424 | expect(isJSONString('["a", "b"]')).toBe(false) 425 | }); 426 | 427 | test('Test invalid JSON { "a": "Missing quote }', () => { 428 | expect(isJSONString('{ "a": }')).toBe(false) 429 | }); 430 | 431 | test('Test invalid JSON null', () => { 432 | expect(isJSONString('')).toBe(false) 433 | }); 434 | 435 | test('Test valid JSON { "a": "yes", "b": "no" } ', () => { 436 | expect(isJSONString('{ "a": "yes", "b": "no" }')).toBe(true) 437 | }); 438 | 439 | test('Test valid nested JSON { "a": "yes", "options": { "opt_a": "yes", "opt_b": "no"} } ', () => { 440 | expect(isJSONString('{ "a": "yes", "options": { "opt_a": "yes", "opt_b": "no"} }')).toBe(true) 441 | }); 442 | 443 | test.each([ 444 | [ 'Uppercase', (x: string) => x.toUpperCase() ], 445 | [ 'uppercase', (x: string) => x.toUpperCase() ], 446 | [ 'lowErCase', (x: string) => x.toLowerCase() ], 447 | [ 'none', (x: string) => x ] 448 | ])('NameTransformation parsing of string %s should pass.', (name: string, transformation: TransformationFunc) => { 449 | const sampleString = '$abcdEFGijk_'; 450 | const parsedTransformation = parseTransformationFunction(name); 451 | expect(parsedTransformation(sampleString)).toEqual(transformation(sampleString)); 452 | }); 453 | 454 | test.each([ 'something', '' ])('NameTransformation parsing of string %s should fail.', (input) => { 455 | expect(() => parseTransformationFunction(input)).toThrow(); 456 | }); 457 | }); 458 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'AWS Secrets Manager GitHub Action' 2 | author: 'AWS Secrets Manager' 3 | description: 'GitHub action for retrieving secrets from AWS Secrets Manager' 4 | branding: 5 | icon: 'cloud' 6 | color: 'orange' 7 | inputs: 8 | secret-ids: 9 | description: 'One or more secret names, secret ARNs, or secret prefixes to retrieve' 10 | required: true 11 | parse-json-secrets: 12 | description: '(Optional) If true, JSON secrets will be deserialized, creating a secret environment variable for each key-value pair.' 13 | required: false 14 | default: 'false' 15 | name-transformation: 16 | description: '(Optional) Transforms environment variable name. Options: uppercase, lowercase, none. Default value: uppercase.' 17 | required: false 18 | default: 'uppercase' 19 | auto-select-family-attempt-timeout: 20 | description: '(Optional) Timeout (ms) for dual-stack DNS first IP connection attempt. Needed for geographically distant GitHub action workers' 21 | required: false 22 | default: '1000' 23 | runs: 24 | using: 'node20' 25 | main: 'dist/index.js' 26 | post: 'dist/cleanup/index.js' 27 | -------------------------------------------------------------------------------- /dist/cleanup.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | var desc = Object.getOwnPropertyDescriptor(m, k); 5 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 6 | desc = { enumerable: true, get: function() { return m[k]; } }; 7 | } 8 | Object.defineProperty(o, k2, desc); 9 | }) : (function(o, m, k, k2) { 10 | if (k2 === undefined) k2 = k; 11 | o[k2] = m[k]; 12 | })); 13 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 14 | Object.defineProperty(o, "default", { enumerable: true, value: v }); 15 | }) : function(o, v) { 16 | o["default"] = v; 17 | }); 18 | var __importStar = (this && this.__importStar) || (function () { 19 | var ownKeys = function(o) { 20 | ownKeys = Object.getOwnPropertyNames || function (o) { 21 | var ar = []; 22 | for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; 23 | return ar; 24 | }; 25 | return ownKeys(o); 26 | }; 27 | return function (mod) { 28 | if (mod && mod.__esModule) return mod; 29 | var result = {}; 30 | if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); 31 | __setModuleDefault(result, mod); 32 | return result; 33 | }; 34 | })(); 35 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 36 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 37 | return new (P || (P = Promise))(function (resolve, reject) { 38 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 39 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 40 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 41 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 42 | }); 43 | }; 44 | Object.defineProperty(exports, "__esModule", { value: true }); 45 | exports.cleanup = cleanup; 46 | const core = __importStar(require("@actions/core")); 47 | const constants_1 = require("./constants"); 48 | const utils_1 = require("./utils"); 49 | /** 50 | * When the GitHub Actions job is done, clean up any environment variables that 51 | * may have been set by the job (https://github.com/aws-actions/configure-aws-credentials/blob/master/cleanup.js) 52 | * 53 | * Environment variables are not intended to be shared across different jobs in 54 | * the same GitHub Actions workflow: GitHub Actions documentation states that 55 | * each job runs in a fresh instance. However, doing our own cleanup will 56 | * give us additional assurance that these environment variables are not shared 57 | * with any other jobs. 58 | */ 59 | function cleanup() { 60 | return __awaiter(this, void 0, void 0, function* () { 61 | try { 62 | const cleanupSecrets = process.env[constants_1.CLEANUP_NAME]; 63 | if (cleanupSecrets) { 64 | // The GitHub Actions toolkit does not have an option to completely unset 65 | // environment variables, so we overwrite the current value with an empty 66 | // string. 67 | JSON.parse(cleanupSecrets).forEach((env) => { 68 | (0, utils_1.cleanVariable)(env); 69 | if (!process.env[env]) { 70 | core.debug(`Removed secret: ${env}`); 71 | } 72 | else { 73 | throw new Error(`Failed to clean secret from environment: ${env}.`); 74 | } 75 | }); 76 | // Clean overall secret list 77 | (0, utils_1.cleanVariable)(constants_1.CLEANUP_NAME); 78 | } 79 | core.info("Cleanup complete."); 80 | } 81 | catch (error) { 82 | if (error instanceof Error) 83 | core.setFailed(error.message); 84 | } 85 | }); 86 | } 87 | cleanup(); 88 | -------------------------------------------------------------------------------- /dist/constants.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.CLEANUP_NAME = exports.LIST_SECRETS_MAX_RESULTS = void 0; 4 | exports.LIST_SECRETS_MAX_RESULTS = 100; 5 | exports.CLEANUP_NAME = 'SECRETS_LIST_CLEAN_UP'; 6 | -------------------------------------------------------------------------------- /dist/sourcemap-register.js: -------------------------------------------------------------------------------- 1 | (()=>{var e={296:e=>{var r=Object.prototype.toString;var n=typeof Buffer!=="undefined"&&typeof Buffer.alloc==="function"&&typeof Buffer.allocUnsafe==="function"&&typeof Buffer.from==="function";function isArrayBuffer(e){return r.call(e).slice(8,-1)==="ArrayBuffer"}function fromArrayBuffer(e,r,t){r>>>=0;var o=e.byteLength-r;if(o<0){throw new RangeError("'offset' is out of bounds")}if(t===undefined){t=o}else{t>>>=0;if(t>o){throw new RangeError("'length' is out of bounds")}}return n?Buffer.from(e.slice(r,r+t)):new Buffer(new Uint8Array(e.slice(r,r+t)))}function fromString(e,r){if(typeof r!=="string"||r===""){r="utf8"}if(!Buffer.isEncoding(r)){throw new TypeError('"encoding" must be a valid string encoding')}return n?Buffer.from(e,r):new Buffer(e,r)}function bufferFrom(e,r,t){if(typeof e==="number"){throw new TypeError('"value" argument must not be a number')}if(isArrayBuffer(e)){return fromArrayBuffer(e,r,t)}if(typeof e==="string"){return fromString(e,r)}return n?Buffer.from(e):new Buffer(e)}e.exports=bufferFrom},599:(e,r,n)=>{e=n.nmd(e);var t=n(927).SourceMapConsumer;var o=n(928);var i;try{i=n(896);if(!i.existsSync||!i.readFileSync){i=null}}catch(e){}var a=n(296);function dynamicRequire(e,r){return e.require(r)}var u=false;var s=false;var l=false;var c="auto";var p={};var f={};var g=/^data:application\/json[^,]+base64,/;var d=[];var h=[];function isInBrowser(){if(c==="browser")return true;if(c==="node")return false;return typeof window!=="undefined"&&typeof XMLHttpRequest==="function"&&!(window.require&&window.module&&window.process&&window.process.type==="renderer")}function hasGlobalProcessEventEmitter(){return typeof process==="object"&&process!==null&&typeof process.on==="function"}function globalProcessVersion(){if(typeof process==="object"&&process!==null){return process.version}else{return""}}function globalProcessStderr(){if(typeof process==="object"&&process!==null){return process.stderr}}function globalProcessExit(e){if(typeof process==="object"&&process!==null&&typeof process.exit==="function"){return process.exit(e)}}function handlerExec(e){return function(r){for(var n=0;n"}var n=this.getLineNumber();if(n!=null){r+=":"+n;var t=this.getColumnNumber();if(t){r+=":"+t}}}var o="";var i=this.getFunctionName();var a=true;var u=this.isConstructor();var s=!(this.isToplevel()||u);if(s){var l=this.getTypeName();if(l==="[object Object]"){l="null"}var c=this.getMethodName();if(i){if(l&&i.indexOf(l)!=0){o+=l+"."}o+=i;if(c&&i.indexOf("."+c)!=i.length-c.length-1){o+=" [as "+c+"]"}}else{o+=l+"."+(c||"")}}else if(u){o+="new "+(i||"")}else if(i){o+=i}else{o+=r;a=false}if(a){o+=" ("+r+")"}return o}function cloneCallSite(e){var r={};Object.getOwnPropertyNames(Object.getPrototypeOf(e)).forEach((function(n){r[n]=/^(?:is|get)/.test(n)?function(){return e[n].call(e)}:e[n]}));r.toString=CallSiteToString;return r}function wrapCallSite(e,r){if(r===undefined){r={nextPosition:null,curPosition:null}}if(e.isNative()){r.curPosition=null;return e}var n=e.getFileName()||e.getScriptNameOrSourceURL();if(n){var t=e.getLineNumber();var o=e.getColumnNumber()-1;var i=/^v(10\.1[6-9]|10\.[2-9][0-9]|10\.[0-9]{3,}|1[2-9]\d*|[2-9]\d|\d{3,}|11\.11)/;var a=i.test(globalProcessVersion())?0:62;if(t===1&&o>a&&!isInBrowser()&&!e.isEval()){o-=a}var u=mapSourcePosition({source:n,line:t,column:o});r.curPosition=u;e=cloneCallSite(e);var s=e.getFunctionName;e.getFunctionName=function(){if(r.nextPosition==null){return s()}return r.nextPosition.name||s()};e.getFileName=function(){return u.source};e.getLineNumber=function(){return u.line};e.getColumnNumber=function(){return u.column+1};e.getScriptNameOrSourceURL=function(){return u.source};return e}var l=e.isEval()&&e.getEvalOrigin();if(l){l=mapEvalOrigin(l);e=cloneCallSite(e);e.getEvalOrigin=function(){return l};return e}return e}function prepareStackTrace(e,r){if(l){p={};f={}}var n=e.name||"Error";var t=e.message||"";var o=n+": "+t;var i={nextPosition:null,curPosition:null};var a=[];for(var u=r.length-1;u>=0;u--){a.push("\n at "+wrapCallSite(r[u],i));i.nextPosition=i.curPosition}i.curPosition=i.nextPosition=null;return o+a.reverse().join("")}function getErrorSource(e){var r=/\n at [^(]+ \((.*):(\d+):(\d+)\)/.exec(e.stack);if(r){var n=r[1];var t=+r[2];var o=+r[3];var a=p[n];if(!a&&i&&i.existsSync(n)){try{a=i.readFileSync(n,"utf8")}catch(e){a=""}}if(a){var u=a.split(/(?:\r\n|\r|\n)/)[t-1];if(u){return n+":"+t+"\n"+u+"\n"+new Array(o).join(" ")+"^"}}}return null}function printErrorAndExit(e){var r=getErrorSource(e);var n=globalProcessStderr();if(n&&n._handle&&n._handle.setBlocking){n._handle.setBlocking(true)}if(r){console.error();console.error(r)}console.error(e.stack);globalProcessExit(1)}function shimEmitUncaughtException(){var e=process.emit;process.emit=function(r){if(r==="uncaughtException"){var n=arguments[1]&&arguments[1].stack;var t=this.listeners(r).length>0;if(n&&!t){return printErrorAndExit(arguments[1])}}return e.apply(this,arguments)}}var S=d.slice(0);var _=h.slice(0);r.wrapCallSite=wrapCallSite;r.getErrorSource=getErrorSource;r.mapSourcePosition=mapSourcePosition;r.retrieveSourceMap=v;r.install=function(r){r=r||{};if(r.environment){c=r.environment;if(["node","browser","auto"].indexOf(c)===-1){throw new Error("environment "+c+" was unknown. Available options are {auto, browser, node}")}}if(r.retrieveFile){if(r.overrideRetrieveFile){d.length=0}d.unshift(r.retrieveFile)}if(r.retrieveSourceMap){if(r.overrideRetrieveSourceMap){h.length=0}h.unshift(r.retrieveSourceMap)}if(r.hookRequire&&!isInBrowser()){var n=dynamicRequire(e,"module");var t=n.prototype._compile;if(!t.__sourceMapSupport){n.prototype._compile=function(e,r){p[r]=e;f[r]=undefined;return t.call(this,e,r)};n.prototype._compile.__sourceMapSupport=true}}if(!l){l="emptyCacheBetweenOperations"in r?r.emptyCacheBetweenOperations:false}if(!u){u=true;Error.prepareStackTrace=prepareStackTrace}if(!s){var o="handleUncaughtExceptions"in r?r.handleUncaughtExceptions:true;try{var i=dynamicRequire(e,"worker_threads");if(i.isMainThread===false){o=false}}catch(e){}if(o&&hasGlobalProcessEventEmitter()){s=true;shimEmitUncaughtException()}}};r.resetRetrieveHandlers=function(){d.length=0;h.length=0;d=S.slice(0);h=_.slice(0);v=handlerExec(h);m=handlerExec(d)}},517:(e,r,n)=>{var t=n(297);var o=Object.prototype.hasOwnProperty;var i=typeof Map!=="undefined";function ArraySet(){this._array=[];this._set=i?new Map:Object.create(null)}ArraySet.fromArray=function ArraySet_fromArray(e,r){var n=new ArraySet;for(var t=0,o=e.length;t=0){return r}}else{var n=t.toSetString(e);if(o.call(this._set,n)){return this._set[n]}}throw new Error('"'+e+'" is not in the set.')};ArraySet.prototype.at=function ArraySet_at(e){if(e>=0&&e{var t=n(158);var o=5;var i=1<>1;return r?-n:n}r.encode=function base64VLQ_encode(e){var r="";var n;var i=toVLQSigned(e);do{n=i&a;i>>>=o;if(i>0){n|=u}r+=t.encode(n)}while(i>0);return r};r.decode=function base64VLQ_decode(e,r,n){var i=e.length;var s=0;var l=0;var c,p;do{if(r>=i){throw new Error("Expected more digits in base 64 VLQ value.")}p=t.decode(e.charCodeAt(r++));if(p===-1){throw new Error("Invalid base64 digit: "+e.charAt(r-1))}c=!!(p&u);p&=a;s=s+(p<{var n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".split("");r.encode=function(e){if(0<=e&&e{r.GREATEST_LOWER_BOUND=1;r.LEAST_UPPER_BOUND=2;function recursiveSearch(e,n,t,o,i,a){var u=Math.floor((n-e)/2)+e;var s=i(t,o[u],true);if(s===0){return u}else if(s>0){if(n-u>1){return recursiveSearch(u,n,t,o,i,a)}if(a==r.LEAST_UPPER_BOUND){return n1){return recursiveSearch(e,u,t,o,i,a)}if(a==r.LEAST_UPPER_BOUND){return u}else{return e<0?-1:e}}}r.search=function search(e,n,t,o){if(n.length===0){return-1}var i=recursiveSearch(-1,n.length,e,n,t,o||r.GREATEST_LOWER_BOUND);if(i<0){return-1}while(i-1>=0){if(t(n[i],n[i-1],true)!==0){break}--i}return i}},24:(e,r,n)=>{var t=n(297);function generatedPositionAfter(e,r){var n=e.generatedLine;var o=r.generatedLine;var i=e.generatedColumn;var a=r.generatedColumn;return o>n||o==n&&a>=i||t.compareByGeneratedPositionsInflated(e,r)<=0}function MappingList(){this._array=[];this._sorted=true;this._last={generatedLine:-1,generatedColumn:0}}MappingList.prototype.unsortedForEach=function MappingList_forEach(e,r){this._array.forEach(e,r)};MappingList.prototype.add=function MappingList_add(e){if(generatedPositionAfter(this._last,e)){this._last=e;this._array.push(e)}else{this._sorted=false;this._array.push(e)}};MappingList.prototype.toArray=function MappingList_toArray(){if(!this._sorted){this._array.sort(t.compareByGeneratedPositionsInflated);this._sorted=true}return this._array};r.P=MappingList},299:(e,r)=>{function swap(e,r,n){var t=e[r];e[r]=e[n];e[n]=t}function randomIntInRange(e,r){return Math.round(e+Math.random()*(r-e))}function doQuickSort(e,r,n,t){if(n{var t;var o=n(297);var i=n(197);var a=n(517).C;var u=n(818);var s=n(299).g;function SourceMapConsumer(e,r){var n=e;if(typeof e==="string"){n=o.parseSourceMapInput(e)}return n.sections!=null?new IndexedSourceMapConsumer(n,r):new BasicSourceMapConsumer(n,r)}SourceMapConsumer.fromSourceMap=function(e,r){return BasicSourceMapConsumer.fromSourceMap(e,r)};SourceMapConsumer.prototype._version=3;SourceMapConsumer.prototype.__generatedMappings=null;Object.defineProperty(SourceMapConsumer.prototype,"_generatedMappings",{configurable:true,enumerable:true,get:function(){if(!this.__generatedMappings){this._parseMappings(this._mappings,this.sourceRoot)}return this.__generatedMappings}});SourceMapConsumer.prototype.__originalMappings=null;Object.defineProperty(SourceMapConsumer.prototype,"_originalMappings",{configurable:true,enumerable:true,get:function(){if(!this.__originalMappings){this._parseMappings(this._mappings,this.sourceRoot)}return this.__originalMappings}});SourceMapConsumer.prototype._charIsMappingSeparator=function SourceMapConsumer_charIsMappingSeparator(e,r){var n=e.charAt(r);return n===";"||n===","};SourceMapConsumer.prototype._parseMappings=function SourceMapConsumer_parseMappings(e,r){throw new Error("Subclasses must implement _parseMappings")};SourceMapConsumer.GENERATED_ORDER=1;SourceMapConsumer.ORIGINAL_ORDER=2;SourceMapConsumer.GREATEST_LOWER_BOUND=1;SourceMapConsumer.LEAST_UPPER_BOUND=2;SourceMapConsumer.prototype.eachMapping=function SourceMapConsumer_eachMapping(e,r,n){var t=r||null;var i=n||SourceMapConsumer.GENERATED_ORDER;var a;switch(i){case SourceMapConsumer.GENERATED_ORDER:a=this._generatedMappings;break;case SourceMapConsumer.ORIGINAL_ORDER:a=this._originalMappings;break;default:throw new Error("Unknown order of iteration.")}var u=this.sourceRoot;a.map((function(e){var r=e.source===null?null:this._sources.at(e.source);r=o.computeSourceURL(u,r,this._sourceMapURL);return{source:r,generatedLine:e.generatedLine,generatedColumn:e.generatedColumn,originalLine:e.originalLine,originalColumn:e.originalColumn,name:e.name===null?null:this._names.at(e.name)}}),this).forEach(e,t)};SourceMapConsumer.prototype.allGeneratedPositionsFor=function SourceMapConsumer_allGeneratedPositionsFor(e){var r=o.getArg(e,"line");var n={source:o.getArg(e,"source"),originalLine:r,originalColumn:o.getArg(e,"column",0)};n.source=this._findSourceIndex(n.source);if(n.source<0){return[]}var t=[];var a=this._findMapping(n,this._originalMappings,"originalLine","originalColumn",o.compareByOriginalPositions,i.LEAST_UPPER_BOUND);if(a>=0){var u=this._originalMappings[a];if(e.column===undefined){var s=u.originalLine;while(u&&u.originalLine===s){t.push({line:o.getArg(u,"generatedLine",null),column:o.getArg(u,"generatedColumn",null),lastColumn:o.getArg(u,"lastGeneratedColumn",null)});u=this._originalMappings[++a]}}else{var l=u.originalColumn;while(u&&u.originalLine===r&&u.originalColumn==l){t.push({line:o.getArg(u,"generatedLine",null),column:o.getArg(u,"generatedColumn",null),lastColumn:o.getArg(u,"lastGeneratedColumn",null)});u=this._originalMappings[++a]}}}return t};r.SourceMapConsumer=SourceMapConsumer;function BasicSourceMapConsumer(e,r){var n=e;if(typeof e==="string"){n=o.parseSourceMapInput(e)}var t=o.getArg(n,"version");var i=o.getArg(n,"sources");var u=o.getArg(n,"names",[]);var s=o.getArg(n,"sourceRoot",null);var l=o.getArg(n,"sourcesContent",null);var c=o.getArg(n,"mappings");var p=o.getArg(n,"file",null);if(t!=this._version){throw new Error("Unsupported version: "+t)}if(s){s=o.normalize(s)}i=i.map(String).map(o.normalize).map((function(e){return s&&o.isAbsolute(s)&&o.isAbsolute(e)?o.relative(s,e):e}));this._names=a.fromArray(u.map(String),true);this._sources=a.fromArray(i,true);this._absoluteSources=this._sources.toArray().map((function(e){return o.computeSourceURL(s,e,r)}));this.sourceRoot=s;this.sourcesContent=l;this._mappings=c;this._sourceMapURL=r;this.file=p}BasicSourceMapConsumer.prototype=Object.create(SourceMapConsumer.prototype);BasicSourceMapConsumer.prototype.consumer=SourceMapConsumer;BasicSourceMapConsumer.prototype._findSourceIndex=function(e){var r=e;if(this.sourceRoot!=null){r=o.relative(this.sourceRoot,r)}if(this._sources.has(r)){return this._sources.indexOf(r)}var n;for(n=0;n1){v.source=l+_[1];l+=_[1];v.originalLine=i+_[2];i=v.originalLine;v.originalLine+=1;v.originalColumn=a+_[3];a=v.originalColumn;if(_.length>4){v.name=c+_[4];c+=_[4]}}m.push(v);if(typeof v.originalLine==="number"){h.push(v)}}}s(m,o.compareByGeneratedPositionsDeflated);this.__generatedMappings=m;s(h,o.compareByOriginalPositions);this.__originalMappings=h};BasicSourceMapConsumer.prototype._findMapping=function SourceMapConsumer_findMapping(e,r,n,t,o,a){if(e[n]<=0){throw new TypeError("Line must be greater than or equal to 1, got "+e[n])}if(e[t]<0){throw new TypeError("Column must be greater than or equal to 0, got "+e[t])}return i.search(e,r,o,a)};BasicSourceMapConsumer.prototype.computeColumnSpans=function SourceMapConsumer_computeColumnSpans(){for(var e=0;e=0){var t=this._generatedMappings[n];if(t.generatedLine===r.generatedLine){var i=o.getArg(t,"source",null);if(i!==null){i=this._sources.at(i);i=o.computeSourceURL(this.sourceRoot,i,this._sourceMapURL)}var a=o.getArg(t,"name",null);if(a!==null){a=this._names.at(a)}return{source:i,line:o.getArg(t,"originalLine",null),column:o.getArg(t,"originalColumn",null),name:a}}}return{source:null,line:null,column:null,name:null}};BasicSourceMapConsumer.prototype.hasContentsOfAllSources=function BasicSourceMapConsumer_hasContentsOfAllSources(){if(!this.sourcesContent){return false}return this.sourcesContent.length>=this._sources.size()&&!this.sourcesContent.some((function(e){return e==null}))};BasicSourceMapConsumer.prototype.sourceContentFor=function SourceMapConsumer_sourceContentFor(e,r){if(!this.sourcesContent){return null}var n=this._findSourceIndex(e);if(n>=0){return this.sourcesContent[n]}var t=e;if(this.sourceRoot!=null){t=o.relative(this.sourceRoot,t)}var i;if(this.sourceRoot!=null&&(i=o.urlParse(this.sourceRoot))){var a=t.replace(/^file:\/\//,"");if(i.scheme=="file"&&this._sources.has(a)){return this.sourcesContent[this._sources.indexOf(a)]}if((!i.path||i.path=="/")&&this._sources.has("/"+t)){return this.sourcesContent[this._sources.indexOf("/"+t)]}}if(r){return null}else{throw new Error('"'+t+'" is not in the SourceMap.')}};BasicSourceMapConsumer.prototype.generatedPositionFor=function SourceMapConsumer_generatedPositionFor(e){var r=o.getArg(e,"source");r=this._findSourceIndex(r);if(r<0){return{line:null,column:null,lastColumn:null}}var n={source:r,originalLine:o.getArg(e,"line"),originalColumn:o.getArg(e,"column")};var t=this._findMapping(n,this._originalMappings,"originalLine","originalColumn",o.compareByOriginalPositions,o.getArg(e,"bias",SourceMapConsumer.GREATEST_LOWER_BOUND));if(t>=0){var i=this._originalMappings[t];if(i.source===n.source){return{line:o.getArg(i,"generatedLine",null),column:o.getArg(i,"generatedColumn",null),lastColumn:o.getArg(i,"lastGeneratedColumn",null)}}}return{line:null,column:null,lastColumn:null}};t=BasicSourceMapConsumer;function IndexedSourceMapConsumer(e,r){var n=e;if(typeof e==="string"){n=o.parseSourceMapInput(e)}var t=o.getArg(n,"version");var i=o.getArg(n,"sections");if(t!=this._version){throw new Error("Unsupported version: "+t)}this._sources=new a;this._names=new a;var u={line:-1,column:0};this._sections=i.map((function(e){if(e.url){throw new Error("Support for url field in sections not implemented.")}var n=o.getArg(e,"offset");var t=o.getArg(n,"line");var i=o.getArg(n,"column");if(t{var t=n(818);var o=n(297);var i=n(517).C;var a=n(24).P;function SourceMapGenerator(e){if(!e){e={}}this._file=o.getArg(e,"file",null);this._sourceRoot=o.getArg(e,"sourceRoot",null);this._skipValidation=o.getArg(e,"skipValidation",false);this._sources=new i;this._names=new i;this._mappings=new a;this._sourcesContents=null}SourceMapGenerator.prototype._version=3;SourceMapGenerator.fromSourceMap=function SourceMapGenerator_fromSourceMap(e){var r=e.sourceRoot;var n=new SourceMapGenerator({file:e.file,sourceRoot:r});e.eachMapping((function(e){var t={generated:{line:e.generatedLine,column:e.generatedColumn}};if(e.source!=null){t.source=e.source;if(r!=null){t.source=o.relative(r,t.source)}t.original={line:e.originalLine,column:e.originalColumn};if(e.name!=null){t.name=e.name}}n.addMapping(t)}));e.sources.forEach((function(t){var i=t;if(r!==null){i=o.relative(r,t)}if(!n._sources.has(i)){n._sources.add(i)}var a=e.sourceContentFor(t);if(a!=null){n.setSourceContent(t,a)}}));return n};SourceMapGenerator.prototype.addMapping=function SourceMapGenerator_addMapping(e){var r=o.getArg(e,"generated");var n=o.getArg(e,"original",null);var t=o.getArg(e,"source",null);var i=o.getArg(e,"name",null);if(!this._skipValidation){this._validateMapping(r,n,t,i)}if(t!=null){t=String(t);if(!this._sources.has(t)){this._sources.add(t)}}if(i!=null){i=String(i);if(!this._names.has(i)){this._names.add(i)}}this._mappings.add({generatedLine:r.line,generatedColumn:r.column,originalLine:n!=null&&n.line,originalColumn:n!=null&&n.column,source:t,name:i})};SourceMapGenerator.prototype.setSourceContent=function SourceMapGenerator_setSourceContent(e,r){var n=e;if(this._sourceRoot!=null){n=o.relative(this._sourceRoot,n)}if(r!=null){if(!this._sourcesContents){this._sourcesContents=Object.create(null)}this._sourcesContents[o.toSetString(n)]=r}else if(this._sourcesContents){delete this._sourcesContents[o.toSetString(n)];if(Object.keys(this._sourcesContents).length===0){this._sourcesContents=null}}};SourceMapGenerator.prototype.applySourceMap=function SourceMapGenerator_applySourceMap(e,r,n){var t=r;if(r==null){if(e.file==null){throw new Error("SourceMapGenerator.prototype.applySourceMap requires either an explicit source file, "+'or the source map\'s "file" property. Both were omitted.')}t=e.file}var a=this._sourceRoot;if(a!=null){t=o.relative(a,t)}var u=new i;var s=new i;this._mappings.unsortedForEach((function(r){if(r.source===t&&r.originalLine!=null){var i=e.originalPositionFor({line:r.originalLine,column:r.originalColumn});if(i.source!=null){r.source=i.source;if(n!=null){r.source=o.join(n,r.source)}if(a!=null){r.source=o.relative(a,r.source)}r.originalLine=i.line;r.originalColumn=i.column;if(i.name!=null){r.name=i.name}}}var l=r.source;if(l!=null&&!u.has(l)){u.add(l)}var c=r.name;if(c!=null&&!s.has(c)){s.add(c)}}),this);this._sources=u;this._names=s;e.sources.forEach((function(r){var t=e.sourceContentFor(r);if(t!=null){if(n!=null){r=o.join(n,r)}if(a!=null){r=o.relative(a,r)}this.setSourceContent(r,t)}}),this)};SourceMapGenerator.prototype._validateMapping=function SourceMapGenerator_validateMapping(e,r,n,t){if(r&&typeof r.line!=="number"&&typeof r.column!=="number"){throw new Error("original.line and original.column are not numbers -- you probably meant to omit "+"the original mapping entirely and only map the generated position. If so, pass "+"null for the original mapping instead of an object with empty or null values.")}if(e&&"line"in e&&"column"in e&&e.line>0&&e.column>=0&&!r&&!n&&!t){return}else if(e&&"line"in e&&"column"in e&&r&&"line"in r&&"column"in r&&e.line>0&&e.column>=0&&r.line>0&&r.column>=0&&n){return}else{throw new Error("Invalid mapping: "+JSON.stringify({generated:e,source:n,original:r,name:t}))}};SourceMapGenerator.prototype._serializeMappings=function SourceMapGenerator_serializeMappings(){var e=0;var r=1;var n=0;var i=0;var a=0;var u=0;var s="";var l;var c;var p;var f;var g=this._mappings.toArray();for(var d=0,h=g.length;d0){if(!o.compareByGeneratedPositionsInflated(c,g[d-1])){continue}l+=","}}l+=t.encode(c.generatedColumn-e);e=c.generatedColumn;if(c.source!=null){f=this._sources.indexOf(c.source);l+=t.encode(f-u);u=f;l+=t.encode(c.originalLine-1-i);i=c.originalLine-1;l+=t.encode(c.originalColumn-n);n=c.originalColumn;if(c.name!=null){p=this._names.indexOf(c.name);l+=t.encode(p-a);a=p}}s+=l}return s};SourceMapGenerator.prototype._generateSourcesContent=function SourceMapGenerator_generateSourcesContent(e,r){return e.map((function(e){if(!this._sourcesContents){return null}if(r!=null){e=o.relative(r,e)}var n=o.toSetString(e);return Object.prototype.hasOwnProperty.call(this._sourcesContents,n)?this._sourcesContents[n]:null}),this)};SourceMapGenerator.prototype.toJSON=function SourceMapGenerator_toJSON(){var e={version:this._version,sources:this._sources.toArray(),names:this._names.toArray(),mappings:this._serializeMappings()};if(this._file!=null){e.file=this._file}if(this._sourceRoot!=null){e.sourceRoot=this._sourceRoot}if(this._sourcesContents){e.sourcesContent=this._generateSourcesContent(e.sources,e.sourceRoot)}return e};SourceMapGenerator.prototype.toString=function SourceMapGenerator_toString(){return JSON.stringify(this.toJSON())};r.x=SourceMapGenerator},565:(e,r,n)=>{var t;var o=n(163).x;var i=n(297);var a=/(\r?\n)/;var u=10;var s="$$$isSourceNode$$$";function SourceNode(e,r,n,t,o){this.children=[];this.sourceContents={};this.line=e==null?null:e;this.column=r==null?null:r;this.source=n==null?null:n;this.name=o==null?null:o;this[s]=true;if(t!=null)this.add(t)}SourceNode.fromStringWithSourceMap=function SourceNode_fromStringWithSourceMap(e,r,n){var t=new SourceNode;var o=e.split(a);var u=0;var shiftNextLine=function(){var e=getNextLine();var r=getNextLine()||"";return e+r;function getNextLine(){return u=0;r--){this.prepend(e[r])}}else if(e[s]||typeof e==="string"){this.children.unshift(e)}else{throw new TypeError("Expected a SourceNode, string, or an array of SourceNodes and strings. Got "+e)}return this};SourceNode.prototype.walk=function SourceNode_walk(e){var r;for(var n=0,t=this.children.length;n0){r=[];for(n=0;n{function getArg(e,r,n){if(r in e){return e[r]}else if(arguments.length===3){return n}else{throw new Error('"'+r+'" is a required argument.')}}r.getArg=getArg;var n=/^(?:([\w+\-.]+):)?\/\/(?:(\w+:\w+)@)?([\w.-]*)(?::(\d+))?(.*)$/;var t=/^data:.+\,.+$/;function urlParse(e){var r=e.match(n);if(!r){return null}return{scheme:r[1],auth:r[2],host:r[3],port:r[4],path:r[5]}}r.urlParse=urlParse;function urlGenerate(e){var r="";if(e.scheme){r+=e.scheme+":"}r+="//";if(e.auth){r+=e.auth+"@"}if(e.host){r+=e.host}if(e.port){r+=":"+e.port}if(e.path){r+=e.path}return r}r.urlGenerate=urlGenerate;function normalize(e){var n=e;var t=urlParse(e);if(t){if(!t.path){return e}n=t.path}var o=r.isAbsolute(n);var i=n.split(/\/+/);for(var a,u=0,s=i.length-1;s>=0;s--){a=i[s];if(a==="."){i.splice(s,1)}else if(a===".."){u++}else if(u>0){if(a===""){i.splice(s+1,u);u=0}else{i.splice(s,2);u--}}}n=i.join("/");if(n===""){n=o?"/":"."}if(t){t.path=n;return urlGenerate(t)}return n}r.normalize=normalize;function join(e,r){if(e===""){e="."}if(r===""){r="."}var n=urlParse(r);var o=urlParse(e);if(o){e=o.path||"/"}if(n&&!n.scheme){if(o){n.scheme=o.scheme}return urlGenerate(n)}if(n||r.match(t)){return r}if(o&&!o.host&&!o.path){o.host=r;return urlGenerate(o)}var i=r.charAt(0)==="/"?r:normalize(e.replace(/\/+$/,"")+"/"+r);if(o){o.path=i;return urlGenerate(o)}return i}r.join=join;r.isAbsolute=function(e){return e.charAt(0)==="/"||n.test(e)};function relative(e,r){if(e===""){e="."}e=e.replace(/\/$/,"");var n=0;while(r.indexOf(e+"/")!==0){var t=e.lastIndexOf("/");if(t<0){return r}e=e.slice(0,t);if(e.match(/^([^\/]+:\/)?\/*$/)){return r}++n}return Array(n+1).join("../")+r.substr(e.length+1)}r.relative=relative;var o=function(){var e=Object.create(null);return!("__proto__"in e)}();function identity(e){return e}function toSetString(e){if(isProtoString(e)){return"$"+e}return e}r.toSetString=o?identity:toSetString;function fromSetString(e){if(isProtoString(e)){return e.slice(1)}return e}r.fromSetString=o?identity:fromSetString;function isProtoString(e){if(!e){return false}var r=e.length;if(r<9){return false}if(e.charCodeAt(r-1)!==95||e.charCodeAt(r-2)!==95||e.charCodeAt(r-3)!==111||e.charCodeAt(r-4)!==116||e.charCodeAt(r-5)!==111||e.charCodeAt(r-6)!==114||e.charCodeAt(r-7)!==112||e.charCodeAt(r-8)!==95||e.charCodeAt(r-9)!==95){return false}for(var n=r-10;n>=0;n--){if(e.charCodeAt(n)!==36){return false}}return true}function compareByOriginalPositions(e,r,n){var t=strcmp(e.source,r.source);if(t!==0){return t}t=e.originalLine-r.originalLine;if(t!==0){return t}t=e.originalColumn-r.originalColumn;if(t!==0||n){return t}t=e.generatedColumn-r.generatedColumn;if(t!==0){return t}t=e.generatedLine-r.generatedLine;if(t!==0){return t}return strcmp(e.name,r.name)}r.compareByOriginalPositions=compareByOriginalPositions;function compareByGeneratedPositionsDeflated(e,r,n){var t=e.generatedLine-r.generatedLine;if(t!==0){return t}t=e.generatedColumn-r.generatedColumn;if(t!==0||n){return t}t=strcmp(e.source,r.source);if(t!==0){return t}t=e.originalLine-r.originalLine;if(t!==0){return t}t=e.originalColumn-r.originalColumn;if(t!==0){return t}return strcmp(e.name,r.name)}r.compareByGeneratedPositionsDeflated=compareByGeneratedPositionsDeflated;function strcmp(e,r){if(e===r){return 0}if(e===null){return 1}if(r===null){return-1}if(e>r){return 1}return-1}function compareByGeneratedPositionsInflated(e,r){var n=e.generatedLine-r.generatedLine;if(n!==0){return n}n=e.generatedColumn-r.generatedColumn;if(n!==0){return n}n=strcmp(e.source,r.source);if(n!==0){return n}n=e.originalLine-r.originalLine;if(n!==0){return n}n=e.originalColumn-r.originalColumn;if(n!==0){return n}return strcmp(e.name,r.name)}r.compareByGeneratedPositionsInflated=compareByGeneratedPositionsInflated;function parseSourceMapInput(e){return JSON.parse(e.replace(/^\)]}'[^\n]*\n/,""))}r.parseSourceMapInput=parseSourceMapInput;function computeSourceURL(e,r,n){r=r||"";if(e){if(e[e.length-1]!=="/"&&r[0]!=="/"){e+="/"}r=e+r}if(n){var t=urlParse(n);if(!t){throw new Error("sourceMapURL could not be parsed")}if(t.path){var o=t.path.lastIndexOf("/");if(o>=0){t.path=t.path.substring(0,o+1)}}r=join(urlGenerate(t),r)}return normalize(r)}r.computeSourceURL=computeSourceURL},927:(e,r,n)=>{n(163).x;r.SourceMapConsumer=n(684).SourceMapConsumer;n(565)},896:e=>{"use strict";e.exports=require("fs")},928:e=>{"use strict";e.exports=require("path")}};var r={};function __webpack_require__(n){var t=r[n];if(t!==undefined){return t.exports}var o=r[n]={id:n,loaded:false,exports:{}};var i=true;try{e[n](o,o.exports,__webpack_require__);i=false}finally{if(i)delete r[n]}o.loaded=true;return o.exports}(()=>{__webpack_require__.nmd=e=>{e.paths=[];if(!e.children)e.children=[];return e}})();if(typeof __webpack_require__!=="undefined")__webpack_require__.ab=__dirname+"/";var n={};__webpack_require__(599).install();module.exports=n})(); -------------------------------------------------------------------------------- /dist/utils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | var desc = Object.getOwnPropertyDescriptor(m, k); 5 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 6 | desc = { enumerable: true, get: function() { return m[k]; } }; 7 | } 8 | Object.defineProperty(o, k2, desc); 9 | }) : (function(o, m, k, k2) { 10 | if (k2 === undefined) k2 = k; 11 | o[k2] = m[k]; 12 | })); 13 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 14 | Object.defineProperty(o, "default", { enumerable: true, value: v }); 15 | }) : function(o, v) { 16 | o["default"] = v; 17 | }); 18 | var __importStar = (this && this.__importStar) || (function () { 19 | var ownKeys = function(o) { 20 | ownKeys = Object.getOwnPropertyNames || function (o) { 21 | var ar = []; 22 | for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; 23 | return ar; 24 | }; 25 | return ownKeys(o); 26 | }; 27 | return function (mod) { 28 | if (mod && mod.__esModule) return mod; 29 | var result = {}; 30 | if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); 31 | __setModuleDefault(result, mod); 32 | return result; 33 | }; 34 | })(); 35 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 36 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 37 | return new (P || (P = Promise))(function (resolve, reject) { 38 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 39 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 40 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 41 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 42 | }); 43 | }; 44 | Object.defineProperty(exports, "__esModule", { value: true }); 45 | exports.buildSecretsList = buildSecretsList; 46 | exports.getSecretsWithPrefix = getSecretsWithPrefix; 47 | exports.getSecretValue = getSecretValue; 48 | exports.injectSecret = injectSecret; 49 | exports.isJSONString = isJSONString; 50 | exports.transformToValidEnvName = transformToValidEnvName; 51 | exports.isSecretArn = isSecretArn; 52 | exports.extractAliasAndSecretIdFromInput = extractAliasAndSecretIdFromInput; 53 | exports.cleanVariable = cleanVariable; 54 | exports.parseTransformationFunction = parseTransformationFunction; 55 | const core = __importStar(require("@actions/core")); 56 | const client_secrets_manager_1 = require("@aws-sdk/client-secrets-manager"); 57 | const constants_1 = require("./constants"); 58 | require("aws-sdk-client-mock-jest"); 59 | /** 60 | * Gets the unique list of all secrets to be requested 61 | * 62 | * @param client: SecretsManager client 63 | * @param configInputs: List of secret names, ARNs, and prefixes provided by user 64 | * @param nameTransformation: Transforms the secret name 65 | */ 66 | function buildSecretsList(client, configInputs, nameTransformation) { 67 | return __awaiter(this, void 0, void 0, function* () { 68 | const finalSecretsList = new Set(); 69 | // Prefix filters should be at least 3 characters, ending in * 70 | const validFilter = new RegExp('^[a-zA-Z0-9\\/_+=.@-]{3,}\\*$'); 71 | for (const configInput of configInputs) { 72 | if (configInput.includes('*')) { 73 | const [secretAlias, secretPrefix] = extractAliasAndSecretIdFromInput(configInput, nameTransformation); 74 | if (!validFilter.test(secretPrefix)) { 75 | throw new Error('Please use a valid prefix search (should be at least 3 characters and end in *)'); 76 | } 77 | // Find and add results for a given prefix 78 | const prefixMatches = yield getSecretsWithPrefix(client, secretPrefix, !!secretAlias); 79 | // Add back the alias, if one was requested 80 | prefixMatches.forEach(secret => finalSecretsList.add(secretAlias ? `${secretAlias},${secret}` : secret)); 81 | } 82 | else { 83 | finalSecretsList.add(configInput); 84 | } 85 | } 86 | return [...finalSecretsList]; 87 | }); 88 | } 89 | /** 90 | * Uses ListSecrets to find secrets for a given prefix 91 | * 92 | * @param client: SecretsManager client 93 | * @param prefix: Name to search for 94 | * @param hasAlias: Flag to indicate that an alias was requested (can only match 1 secret) 95 | */ 96 | function getSecretsWithPrefix(client, prefix, hasAlias) { 97 | return __awaiter(this, void 0, void 0, function* () { 98 | const params = { 99 | Filters: [ 100 | { 101 | Key: "name", 102 | Values: [ 103 | prefix.replace('*', ''), 104 | ] 105 | }, 106 | ], 107 | MaxResults: constants_1.LIST_SECRETS_MAX_RESULTS, 108 | }; 109 | const response = yield client.send(new client_secrets_manager_1.ListSecretsCommand(params)); 110 | if (response.SecretList) { 111 | const secretsList = response.SecretList; 112 | if (secretsList.length === 0) { 113 | throw new Error(`No matching secrets were returned for prefix "${prefix}".`); 114 | } 115 | else if (hasAlias && secretsList.length > 1) { 116 | // If an alias was requested, we cannot match more than one result 117 | throw new Error(`A unique alias was requested for prefix "${prefix}", but the search result for this prefix returned multiple results.`); 118 | } 119 | else if (response.NextToken) { 120 | // If there is a second page of results, this exceeds the max number of matches 121 | throw new Error(`A search for prefix "${prefix}" matched more than the maximum of ${constants_1.LIST_SECRETS_MAX_RESULTS} secrets per prefix.`); 122 | } 123 | return secretsList.reduce((foundSecrets, secret) => { 124 | if (secret.Name) { 125 | foundSecrets.push(secret.Name); 126 | } 127 | return foundSecrets; 128 | }, []); 129 | } 130 | else { 131 | throw new Error('Invalid response from ListSecrets occurred'); 132 | } 133 | }); 134 | } 135 | /** 136 | * Retrieves a secret from Secrets Manager 137 | * 138 | * @param client: SecretsManager client 139 | * @param secretId: The name or full ARN of a secret 140 | * @returns SecretValueResponse 141 | */ 142 | function getSecretValue(client, secretId) { 143 | return __awaiter(this, void 0, void 0, function* () { 144 | let secretValue = ''; 145 | const data = yield client.send(new client_secrets_manager_1.GetSecretValueCommand({ SecretId: secretId })); 146 | if (data.SecretString) { 147 | secretValue = data.SecretString; 148 | } 149 | else if (data.SecretBinary) { 150 | // Only string and JSON string values are supported in Github env 151 | secretValue = Buffer.from(data.SecretBinary).toString('ascii'); 152 | } 153 | if (!(data.Name)) { 154 | throw new Error('Invalid name for secret'); 155 | } 156 | return { 157 | name: data.Name, 158 | secretValue 159 | }; 160 | }); 161 | } 162 | /** 163 | * Transforms and injects secret as a masked environmental variable 164 | * 165 | * @param secretName: Name of the secret 166 | * @param secretValue: Value to set for secret 167 | * @param parseJsonSecrets: Indicates whether to deserialize JSON secrets 168 | * @param nameTransformation: Transforms the secret name 169 | * @param tempEnvName: If parsing JSON secrets, contains the current name for the env variable 170 | */ 171 | function injectSecret(secretName, secretValue, parseJsonSecrets, nameTransformation, tempEnvName) { 172 | let secretsToCleanup = []; 173 | if (parseJsonSecrets && isJSONString(secretValue)) { 174 | // Recursively parses json secrets 175 | const secretMap = JSON.parse(secretValue); 176 | for (const k in secretMap) { 177 | const keyValue = typeof secretMap[k] === 'string' ? secretMap[k] : JSON.stringify(secretMap[k]); 178 | // Append the current key to the name of the env variable and check to avoid prepending an underscore 179 | const newEnvName = [ 180 | tempEnvName || transformToValidEnvName(secretName, nameTransformation, false), 181 | transformToValidEnvName(k, nameTransformation, true) 182 | ] 183 | .filter(elem => elem) // Uses truthy-ness of elem to determine if it remains 184 | .join("_"); // Join the remaining elements with an underscore 185 | secretsToCleanup = [...secretsToCleanup, ...injectSecret(secretName, keyValue, parseJsonSecrets, nameTransformation, newEnvName)]; 186 | } 187 | } 188 | else { 189 | const envName = transformToValidEnvName(tempEnvName ? tempEnvName : secretName, nameTransformation); 190 | // Fail the action if this variable name is already in use, or is our cleanup name 191 | if (process.env[envName] || envName === constants_1.CLEANUP_NAME) { 192 | throw new Error(`The environment name '${envName}' is already in use. Please use an alias to ensure that each secret has a unique environment name`); 193 | } 194 | // Inject a single secret 195 | core.setSecret(secretValue); 196 | // Export variable 197 | core.debug(`Injecting secret ${secretName} as environment variable '${envName}'.`); 198 | core.exportVariable(envName, secretValue); 199 | secretsToCleanup.push(envName); 200 | } 201 | return secretsToCleanup; 202 | } 203 | /* 204 | * Checks if the given secret is a valid JSON value 205 | */ 206 | function isJSONString(secretValue) { 207 | try { 208 | // Not valid JSON if the parsed result is null/falsy, not an object, or is an array 209 | const parsedObject = JSON.parse(secretValue); 210 | return !!parsedObject && (typeof parsedObject === 'object') && !Array.isArray(parsedObject); 211 | } 212 | catch (_a) { 213 | // Not JSON if the string fails to parse 214 | return false; 215 | } 216 | } 217 | /* 218 | * Transforms the secret name into a valid environmental variable name 219 | * It should consist of only upper case letters, digits, and underscores and cannot begin with a number 220 | */ 221 | function transformToValidEnvName(secretName, nameTransformation, hasPrefix = false) { 222 | // Leading digits are invalid 223 | if (!hasPrefix && secretName.match(/^[0-9]/)) { 224 | secretName = '_'.concat(secretName); 225 | } 226 | // Remove invalid characters 227 | secretName = secretName.replace(/[^a-zA-Z0-9_]/g, '_'); 228 | // Apply the name transformation. When no transformation is defined fallback to the "uppercase" transformation 229 | return nameTransformation ? nameTransformation(secretName) : secretName.toUpperCase(); 230 | } 231 | /** 232 | * Checks if the given secretId is an ARN 233 | * 234 | * @param secretId: Value to test 235 | * @returns Boolean 236 | */ 237 | function isSecretArn(secretId) { 238 | const validArn = new RegExp('^arn:aws:secretsmanager:.*:[0-9]{12,}:secret:.*$'); 239 | return validArn.test(secretId); 240 | } 241 | /* 242 | * Separates a secret alias from the secret name/arn, if one was provided 243 | */ 244 | function extractAliasAndSecretIdFromInput(input, nameTransformation) { 245 | const parsedInput = input.split(','); 246 | if (parsedInput.length > 1) { 247 | const alias = parsedInput[0].trim(); 248 | const secretId = parsedInput[1].trim(); 249 | // Validate that the alias is valid environment name 250 | const validateEnvName = transformToValidEnvName(alias, nameTransformation); 251 | if (alias !== validateEnvName) { 252 | throw new Error(`The alias '${alias}' is not a valid environment name. Please verify that it has uppercase letters, numbers, and underscore only.`); 253 | } 254 | // Return [alias, id] 255 | return [alias, secretId]; 256 | } 257 | // No alias 258 | return [undefined, input.trim()]; 259 | } 260 | /* 261 | * Cleans up an environment variable 262 | */ 263 | function cleanVariable(variableName) { 264 | core.exportVariable(variableName, ''); 265 | delete process.env[variableName]; 266 | } 267 | /* 268 | * Converts name of the transformation to the actual function that performs the transformation. 269 | */ 270 | function parseTransformationFunction(config) { 271 | switch (config.toLowerCase()) { 272 | case 'uppercase': 273 | return (input) => input.toUpperCase(); 274 | case 'lowercase': 275 | return (input) => input.toLowerCase(); 276 | case 'none': 277 | return (input) => input; 278 | default: 279 | throw new Error(`'${config}' is unsupported transformation name. Allowed options are: 'uppercase', 'lowercase' and 'none'`); 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | coverageThreshold: { 5 | global: { 6 | "branches": 90, 7 | "functions": 85, 8 | "lines": 90, 9 | "statements": 90 10 | } 11 | } 12 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-secretsmanager-get-secrets", 3 | "version": "1.0.0", 4 | "description": "GitHub action for retrieving secrets from AWS Secrets Manager", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "test": "npm run lint && jest --coverage --verbose __tests__", 8 | "integration-test": "jest --coverage --verbose", 9 | "lint": "eslint src/**.ts", 10 | "fix": "eslint src/** --fix", 11 | "build": "tsc && ncc build ./src/index.ts --source-map --license licenses.txt && ncc build ./src/cleanup.ts -o dist/cleanup" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/aws-actions/aws-secretsmanager-get-secrets.git" 16 | }, 17 | "keywords": [ 18 | "AWS", 19 | "SecretsManager", 20 | "GitHub", 21 | "Actions" 22 | ], 23 | "author": "", 24 | "license": "ISC", 25 | "bugs": { 26 | "url": "https://github.com/aws-actions/aws-secretsmanager-get-secrets/issues" 27 | }, 28 | "homepage": "https://github.com/aws-actions/aws-secretsmanager-get-secrets#readme", 29 | "dependencies": { 30 | "@actions/core": "^1.10.0", 31 | "@actions/github": "^6.0.0", 32 | "@aws-sdk/client-secrets-manager": "^3.606.0" 33 | }, 34 | "devDependencies": { 35 | "@types/jest": "^29.5.12", 36 | "@typescript-eslint/eslint-plugin": "^6.20.0", 37 | "@typescript-eslint/parser": "^6.20.0", 38 | "@vercel/ncc": "^0.38.1", 39 | "aws-sdk-client-mock": "^4.0.1", 40 | "aws-sdk-client-mock-jest": "^4.0.1", 41 | "eslint": "^8.57.0", 42 | "jest": "^29.7.0", 43 | "ts-jest": "^29.1.5" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/cleanup.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import { CLEANUP_NAME } from "./constants"; 3 | import { cleanVariable } from "./utils"; 4 | 5 | 6 | /** 7 | * When the GitHub Actions job is done, clean up any environment variables that 8 | * may have been set by the job (https://github.com/aws-actions/configure-aws-credentials/blob/master/cleanup.js) 9 | * 10 | * Environment variables are not intended to be shared across different jobs in 11 | * the same GitHub Actions workflow: GitHub Actions documentation states that 12 | * each job runs in a fresh instance. However, doing our own cleanup will 13 | * give us additional assurance that these environment variables are not shared 14 | * with any other jobs. 15 | */ 16 | export async function cleanup(): Promise { 17 | try { 18 | const cleanupSecrets = process.env[CLEANUP_NAME]; 19 | 20 | if (cleanupSecrets){ 21 | // The GitHub Actions toolkit does not have an option to completely unset 22 | // environment variables, so we overwrite the current value with an empty 23 | // string. 24 | JSON.parse(cleanupSecrets).forEach((env: string) => { 25 | cleanVariable(env); 26 | 27 | if (!process.env[env]){ 28 | core.debug(`Removed secret: ${env}`); 29 | } else { 30 | throw new Error(`Failed to clean secret from environment: ${env}.`); 31 | } 32 | }); 33 | 34 | // Clean overall secret list 35 | cleanVariable(CLEANUP_NAME); 36 | } 37 | 38 | core.info("Cleanup complete."); 39 | } catch (error) { 40 | if (error instanceof Error) core.setFailed(error.message) 41 | } 42 | } 43 | 44 | 45 | 46 | cleanup(); -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const LIST_SECRETS_MAX_RESULTS = 100; 2 | export const CLEANUP_NAME = 'SECRETS_LIST_CLEAN_UP'; 3 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import { setDefaultAutoSelectFamilyAttemptTimeout } from 'net'; 3 | import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager"; 4 | import { 5 | buildSecretsList, 6 | isSecretArn, 7 | getSecretValue, 8 | injectSecret, 9 | extractAliasAndSecretIdFromInput, 10 | SecretValueResponse, isJSONString, 11 | parseTransformationFunction 12 | } from "./utils"; 13 | import { CLEANUP_NAME } from "./constants"; 14 | 15 | export async function run(): Promise { 16 | try { 17 | // Node 20 introduced automatic family selection for dual-stack endpoints. When the runner 18 | // sits far away from the secrets manager endpoint it sometimes timeouts on negotiation between 19 | // A and AAAA records. This behaviour was described in the https://github.com/nodejs/node/issues/54359 20 | // The default value is 1s. We allow configuring this timeout through the 21 | // 'auto-select-family-attempt-timeout' parameter to help prevent flaky integration tests 22 | 23 | const timeout = Number(core.getInput('auto-select-family-attempt-timeout')); 24 | 25 | if (timeout < 10 || Number.isNaN(timeout)) { 26 | core.setFailed(`Invalid value for 'auto-select-family-attempt-timeout': ${timeout}. Must be a number greater than or equal to 10.`); 27 | return; 28 | } 29 | 30 | setDefaultAutoSelectFamilyAttemptTimeout(timeout); 31 | 32 | 33 | 34 | // Default client region is set by configure-aws-credentials 35 | const client : SecretsManagerClient = new SecretsManagerClient({region: process.env.AWS_DEFAULT_REGION, customUserAgent: "github-action"}); 36 | const secretConfigInputs: string[] = [...new Set(core.getMultilineInput('secret-ids'))]; 37 | const parseJsonSecrets = core.getBooleanInput('parse-json-secrets'); 38 | const nameTransformation = parseTransformationFunction(core.getInput('name-transformation')); 39 | 40 | // Get final list of secrets to request 41 | core.info('Building secrets list...'); 42 | const secretIds: string[] = await buildSecretsList(client, secretConfigInputs, nameTransformation); 43 | 44 | // Keep track of secret names that will need to be cleaned from the environment 45 | let secretsToCleanup = [] as string[]; 46 | 47 | core.info('Your secret names may be transformed in order to be valid environment variables (see README). Enable Debug logging in order to view the new environment names.'); 48 | 49 | // Get and inject secret values 50 | for (let secretId of secretIds) { 51 | // Optionally let user set an alias, i.e. `ENV_NAME,secret_name` 52 | let secretAlias: string | undefined = undefined; 53 | [secretAlias, secretId] = extractAliasAndSecretIdFromInput(secretId, nameTransformation); 54 | 55 | // Retrieves the secret name also, if the value is an ARN 56 | const isArn = isSecretArn(secretId); 57 | 58 | try { 59 | const secretValueResponse : SecretValueResponse = await getSecretValue(client, secretId); 60 | const secretValue = secretValueResponse.secretValue; 61 | 62 | // Catch if blank prefix is specified but no json is parsed to avoid blank environment variable 63 | if ((secretAlias === '') && !(parseJsonSecrets && isJSONString(secretValue))) { 64 | secretAlias = undefined; 65 | } 66 | 67 | if (secretAlias === undefined) { 68 | secretAlias = isArn ? secretValueResponse.name : secretId; 69 | } 70 | 71 | const injectedSecrets = injectSecret(secretAlias, secretValue, parseJsonSecrets, nameTransformation); 72 | secretsToCleanup = [...secretsToCleanup, ...injectedSecrets]; 73 | } catch (err) { 74 | // Fail action for any error 75 | core.setFailed(`Failed to fetch secret: '${secretId}'. Error: ${err}.`) 76 | } 77 | } 78 | 79 | // Get existing clean up list 80 | const existingCleanupSecrets = process.env[CLEANUP_NAME]; 81 | if (existingCleanupSecrets) { 82 | secretsToCleanup = [...JSON.parse(existingCleanupSecrets), ...secretsToCleanup]; 83 | } 84 | 85 | // Export the names of variables to clean up after completion 86 | core.exportVariable(CLEANUP_NAME, JSON.stringify(secretsToCleanup)); 87 | 88 | core.info("Completed adding secrets."); 89 | } catch (error) { 90 | if (error instanceof Error) core.setFailed(error.message) 91 | } 92 | } 93 | 94 | 95 | 96 | run(); -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import { 3 | SecretsManagerClient, 4 | GetSecretValueCommand, 5 | ListSecretsCommand, 6 | ListSecretsResponse, 7 | ListSecretsCommandInput 8 | } from "@aws-sdk/client-secrets-manager"; 9 | import { CLEANUP_NAME, LIST_SECRETS_MAX_RESULTS } from "./constants"; 10 | import "aws-sdk-client-mock-jest"; 11 | 12 | export interface SecretValueResponse { 13 | name: string, 14 | secretValue: string 15 | } 16 | 17 | export type TransformationFunc = (input: string) => string; 18 | 19 | /** 20 | * Gets the unique list of all secrets to be requested 21 | * 22 | * @param client: SecretsManager client 23 | * @param configInputs: List of secret names, ARNs, and prefixes provided by user 24 | * @param nameTransformation: Transforms the secret name 25 | */ 26 | export async function buildSecretsList(client: SecretsManagerClient, configInputs: string[], nameTransformation?: TransformationFunc): Promise { 27 | const finalSecretsList = new Set(); 28 | 29 | // Prefix filters should be at least 3 characters, ending in * 30 | const validFilter = new RegExp('^[a-zA-Z0-9\\/_+=.@-]{3,}\\*$'); 31 | 32 | for (const configInput of configInputs) { 33 | if (configInput.includes('*')) { 34 | const [secretAlias, secretPrefix] = extractAliasAndSecretIdFromInput(configInput, nameTransformation); 35 | 36 | if (!validFilter.test(secretPrefix)) { 37 | throw new Error('Please use a valid prefix search (should be at least 3 characters and end in *)'); 38 | } 39 | 40 | // Find and add results for a given prefix 41 | const prefixMatches: string[] = await getSecretsWithPrefix(client, secretPrefix, !!secretAlias); 42 | 43 | // Add back the alias, if one was requested 44 | prefixMatches.forEach(secret => finalSecretsList.add(secretAlias ? `${secretAlias},${secret}` : secret)); 45 | } else { 46 | finalSecretsList.add(configInput); 47 | } 48 | } 49 | 50 | return [...finalSecretsList]; 51 | } 52 | 53 | /** 54 | * Uses ListSecrets to find secrets for a given prefix 55 | * 56 | * @param client: SecretsManager client 57 | * @param prefix: Name to search for 58 | * @param hasAlias: Flag to indicate that an alias was requested (can only match 1 secret) 59 | */ 60 | export async function getSecretsWithPrefix(client: SecretsManagerClient, prefix: string, hasAlias: boolean): Promise { 61 | const params = { 62 | Filters: [ 63 | { 64 | Key: "name", 65 | Values: [ 66 | prefix.replace('*', ''), 67 | ] 68 | }, 69 | ], 70 | MaxResults: LIST_SECRETS_MAX_RESULTS, 71 | } as ListSecretsCommandInput; 72 | 73 | const response: ListSecretsResponse = await client.send(new ListSecretsCommand(params)); 74 | 75 | if (response.SecretList){ 76 | const secretsList = response.SecretList; 77 | if (secretsList.length === 0){ 78 | throw new Error(`No matching secrets were returned for prefix "${prefix}".`); 79 | } else if (hasAlias && secretsList.length > 1){ 80 | // If an alias was requested, we cannot match more than one result 81 | throw new Error(`A unique alias was requested for prefix "${prefix}", but the search result for this prefix returned multiple results.`); 82 | } else if (response.NextToken) { 83 | // If there is a second page of results, this exceeds the max number of matches 84 | throw new Error(`A search for prefix "${prefix}" matched more than the maximum of ${LIST_SECRETS_MAX_RESULTS} secrets per prefix.`); 85 | } 86 | 87 | return secretsList.reduce((foundSecrets, secret) => { 88 | if (secret.Name) { 89 | foundSecrets.push(secret.Name); 90 | } 91 | return foundSecrets; 92 | }, [] as string[]); 93 | } else { 94 | throw new Error('Invalid response from ListSecrets occurred'); 95 | } 96 | } 97 | 98 | /** 99 | * Retrieves a secret from Secrets Manager 100 | * 101 | * @param client: SecretsManager client 102 | * @param secretId: The name or full ARN of a secret 103 | * @returns SecretValueResponse 104 | */ 105 | export async function getSecretValue(client: SecretsManagerClient, secretId: string): Promise { 106 | let secretValue = ''; 107 | 108 | const data = await client.send(new GetSecretValueCommand({SecretId: secretId})); 109 | 110 | if (data.SecretString) { 111 | secretValue = data.SecretString as string; 112 | } else if (data.SecretBinary) { 113 | // Only string and JSON string values are supported in Github env 114 | secretValue = Buffer.from(data.SecretBinary).toString('ascii'); 115 | } 116 | 117 | if (!(data.Name)){ 118 | throw new Error('Invalid name for secret'); 119 | } 120 | 121 | return { 122 | name: data.Name, 123 | secretValue 124 | } as SecretValueResponse; 125 | } 126 | 127 | /** 128 | * Transforms and injects secret as a masked environmental variable 129 | * 130 | * @param secretName: Name of the secret 131 | * @param secretValue: Value to set for secret 132 | * @param parseJsonSecrets: Indicates whether to deserialize JSON secrets 133 | * @param nameTransformation: Transforms the secret name 134 | * @param tempEnvName: If parsing JSON secrets, contains the current name for the env variable 135 | */ 136 | export function injectSecret( 137 | secretName: string, 138 | secretValue: string, 139 | parseJsonSecrets: boolean, 140 | nameTransformation?: TransformationFunc, 141 | tempEnvName?: string): string[] { 142 | let secretsToCleanup = [] as string[]; 143 | if(parseJsonSecrets && isJSONString(secretValue)){ 144 | // Recursively parses json secrets 145 | const secretMap = JSON.parse(secretValue) as Record; 146 | 147 | for (const k in secretMap) { 148 | const keyValue = typeof secretMap[k] === 'string' ? secretMap[k] as string : JSON.stringify(secretMap[k]); 149 | 150 | // Append the current key to the name of the env variable and check to avoid prepending an underscore 151 | const newEnvName = [ 152 | tempEnvName || transformToValidEnvName(secretName, nameTransformation, false), 153 | transformToValidEnvName(k, nameTransformation, true) 154 | ] 155 | .filter(elem => elem) // Uses truthy-ness of elem to determine if it remains 156 | .join("_"); // Join the remaining elements with an underscore 157 | 158 | secretsToCleanup = [...secretsToCleanup, ...injectSecret(secretName, keyValue, parseJsonSecrets, nameTransformation, newEnvName)]; 159 | } 160 | } else { 161 | const envName = transformToValidEnvName(tempEnvName ? tempEnvName : secretName, nameTransformation); 162 | 163 | // Fail the action if this variable name is already in use, or is our cleanup name 164 | if (process.env[envName] || envName === CLEANUP_NAME){ 165 | throw new Error(`The environment name '${envName}' is already in use. Please use an alias to ensure that each secret has a unique environment name`); 166 | } 167 | 168 | // Inject a single secret 169 | core.setSecret(secretValue); 170 | 171 | // Export variable 172 | core.debug(`Injecting secret ${secretName} as environment variable '${envName}'.`); 173 | core.exportVariable(envName, secretValue); 174 | secretsToCleanup.push(envName); 175 | } 176 | 177 | return secretsToCleanup; 178 | } 179 | 180 | /* 181 | * Checks if the given secret is a valid JSON value 182 | */ 183 | export function isJSONString(secretValue: string): boolean { 184 | try { 185 | // Not valid JSON if the parsed result is null/falsy, not an object, or is an array 186 | const parsedObject = JSON.parse(secretValue); 187 | return !!parsedObject && (typeof parsedObject === 'object') && !Array.isArray(parsedObject); 188 | } catch { 189 | // Not JSON if the string fails to parse 190 | return false; 191 | } 192 | } 193 | 194 | /* 195 | * Transforms the secret name into a valid environmental variable name 196 | * It should consist of only upper case letters, digits, and underscores and cannot begin with a number 197 | */ 198 | export function transformToValidEnvName(secretName: string, nameTransformation?: TransformationFunc, hasPrefix : boolean = false): string { 199 | // Leading digits are invalid 200 | if (!hasPrefix && secretName.match(/^[0-9]/)) { 201 | secretName = '_'.concat(secretName); 202 | } 203 | 204 | // Remove invalid characters 205 | secretName = secretName.replace(/[^a-zA-Z0-9_]/g, '_'); 206 | 207 | // Apply the name transformation. When no transformation is defined fallback to the "uppercase" transformation 208 | return nameTransformation ? nameTransformation(secretName) : secretName.toUpperCase(); 209 | } 210 | 211 | 212 | /** 213 | * Checks if the given secretId is an ARN 214 | * 215 | * @param secretId: Value to test 216 | * @returns Boolean 217 | */ 218 | export function isSecretArn(secretId: string): boolean { 219 | const validArn = new RegExp('^arn:aws:secretsmanager:.*:[0-9]{12,}:secret:.*$'); 220 | return validArn.test(secretId); 221 | } 222 | 223 | /* 224 | * Separates a secret alias from the secret name/arn, if one was provided 225 | */ 226 | export function extractAliasAndSecretIdFromInput(input: string, nameTransformation?: TransformationFunc): [undefined | string, string] { 227 | const parsedInput = input.split(','); 228 | if (parsedInput.length > 1){ 229 | const alias = parsedInput[0].trim(); 230 | const secretId = parsedInput[1].trim(); 231 | 232 | // Validate that the alias is valid environment name 233 | const validateEnvName = transformToValidEnvName(alias, nameTransformation); 234 | if (alias !== validateEnvName){ 235 | throw new Error(`The alias '${alias}' is not a valid environment name. Please verify that it has uppercase letters, numbers, and underscore only.`); 236 | } 237 | 238 | // Return [alias, id] 239 | return [alias, secretId]; 240 | } 241 | 242 | // No alias 243 | return [ undefined , input.trim() ]; 244 | } 245 | 246 | /* 247 | * Cleans up an environment variable 248 | */ 249 | export function cleanVariable(variableName: string) { 250 | core.exportVariable(variableName, ''); 251 | delete process.env[variableName]; 252 | } 253 | 254 | /* 255 | * Converts name of the transformation to the actual function that performs the transformation. 256 | */ 257 | export function parseTransformationFunction(config: string): TransformationFunc { 258 | switch (config.toLowerCase()) { 259 | case 'uppercase': 260 | return (input: string) => input.toUpperCase(); 261 | case 'lowercase': 262 | return (input: string) => input.toLowerCase(); 263 | case 'none': 264 | return (input: string) => input; 265 | default: 266 | throw new Error(`'${config}' is unsupported transformation name. Allowed options are: 'uppercase', 'lowercase' and 'none'`); 267 | } 268 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 4 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 5 | "outDir": "./dist", /* Redirect output structure to the directory. */ 6 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 7 | "strict": true, /* Enable all strict type-checking options. */ 8 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 9 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 10 | }, 11 | "exclude": ["node_modules", "**/*.test.ts"], 12 | "include": ["./src"] 13 | } --------------------------------------------------------------------------------