1
0
Fork 0
mirror of synced 2026-06-05 18:38:19 +00:00

Compare commits

..

1 commit

Author SHA1 Message Date
Tom Keller
4dc48c9eea fix: skip credential check on output-env-credentials: false
Closes #1554.
2026-05-14 13:23:02 -07:00
16 changed files with 22622 additions and 19503 deletions

View file

@ -1,5 +1,5 @@
{ {
".release-please-manifest.json": "4.0.2", ".release-please-manifest.json": "4.0.2",
"package.json": "6.0.0", "package.json": "6.0.0",
".": "6.2.0" ".": "6.1.1"
} }

View file

@ -2,34 +2,6 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [6.2.0](https://github.com/aws-actions/configure-aws-credentials/compare/v6.1.3...v6.2.0) (2026-06-01)
### Features
* add additional session tags by default ([#1775](https://github.com/aws-actions/configure-aws-credentials/issues/1775)) ([e0ba768](https://github.com/aws-actions/configure-aws-credentials/commit/e0ba7685077379a14a82d01fefd511490344ebfc))
* add more retry logic and better logging ([#1764](https://github.com/aws-actions/configure-aws-credentials/issues/1764)) ([540d0c1](https://github.com/aws-actions/configure-aws-credentials/commit/540d0c13aedb8d55501d220bd2f0b3cdedfe84e8))
* add regex validation to role-session-name ([#1765](https://github.com/aws-actions/configure-aws-credentials/issues/1765)) ([e354499](https://github.com/aws-actions/configure-aws-credentials/commit/e35449909c6ede5083a48ba4b8bbfaaa1cf09ba1))
* Allow custom session tags to be passed when assuming a role ([#1759](https://github.com/aws-actions/configure-aws-credentials/issues/1759)) ([61f50f6](https://github.com/aws-actions/configure-aws-credentials/commit/61f50f630f383628add73c1eab3f1935ba07da2b))
* expose run id in STS client user-agent ([#1774](https://github.com/aws-actions/configure-aws-credentials/issues/1774)) ([29d1be3](https://github.com/aws-actions/configure-aws-credentials/commit/29d1be30273e7ef371d59fccf6ec54572c64ec89))
* support custom STS endpoints ([#1762](https://github.com/aws-actions/configure-aws-credentials/issues/1762)) ([8d52d05](https://github.com/aws-actions/configure-aws-credentials/commit/8d52d05d7a4521fa52b39de50cb6114b12e5c332))
### Bug Fixes
* skip credential check on output-env-credentials: false ([#1778](https://github.com/aws-actions/configure-aws-credentials/issues/1778)) ([58e7c47](https://github.com/aws-actions/configure-aws-credentials/commit/58e7c47adf77846879008deadfeeef8a6969fe6c))
* assumeRole failing from session tag size too large ([#1808](https://github.com/aws-actions/configure-aws-credentials/issues/1808)) ([d6f5dc3](https://github.com/aws-actions/configure-aws-credentials/commit/d6f5dc331b44474b19a52caaf85fa4d637b13c8e))
## [6.1.3](https://github.com/aws-actions/configure-aws-credentials/compare/v6.1.2...v6.1.3) (2026-05-28)
### Bug Fixes
* fix: allow kubelet token symlink in [#1805](https://github.com/aws-actions/configure-aws-credentials/issues/1805)
## [6.1.2](https://github.com/aws-actions/configure-aws-credentials/compare/v6.1.1...v6.1.2) (2026-05-26)
### Bug Fixes
* additional filesystem checks ([#1799](https://github.com/aws-actions/configure-aws-credentials/issues/1799)) ([c39f282](https://github.com/aws-actions/configure-aws-credentials/commit/c39f282697aca8a78c522ecf1f7da9899a31432c))
## [6.1.1](https://github.com/aws-actions/configure-aws-credentials/compare/v6.1.0...v6.1.1) (2026-05-05) ## [6.1.1](https://github.com/aws-actions/configure-aws-credentials/compare/v6.1.0...v6.1.1) (2026-05-05)

133
README.md
View file

@ -168,7 +168,6 @@ detail.
| role-session-name | Defaults to "GitHubActions", but may be changed if required. | No | | role-session-name | Defaults to "GitHubActions", but may be changed if required. | No |
| role-skip-session-tagging | Skips session tagging if set. | No | | role-skip-session-tagging | Skips session tagging if set. | No |
| transitive-tag-keys | Define a list of transitive tag keys to pass when assuming a role. | No | | transitive-tag-keys | Define a list of transitive tag keys to pass when assuming a role. | No |
| custom-tags | Additional tags to apply to the assumed role session. Must be a JSON object provided as a string. Custom tags are not usable with OIDC or web identity token authentication. | No |
| inline-session-policy | You may further restrict the assumed role policy by defining an inline policy here. | No | | inline-session-policy | You may further restrict the assumed role policy by defining an inline policy here. | No |
| managed-session-policies | You may further restrict the assumed role policy by specifying a managed policy here. | No | | managed-session-policies | You may further restrict the assumed role policy by specifying a managed policy here. | No |
| output-credentials | When set, outputs fetched credentials as action step output. (Outputs aws-access-key-id, aws-secret-access-key, aws-session-token, aws-account-id, authenticated-arn, and aws-expiration). Defaults to false. | No | | output-credentials | When set, outputs fetched credentials as action step output. (Outputs aws-access-key-id, aws-secret-access-key, aws-session-token, aws-account-id, authenticated-arn, and aws-expiration). Defaults to false. | No |
@ -181,8 +180,6 @@ detail.
| allowed-account-ids | A comma-delimited list of expected AWS account IDs. The action will fail if we receive credentials for the wrong account. | No | | allowed-account-ids | A comma-delimited list of expected AWS account IDs. The action will fail if we receive credentials for the wrong account. | No |
| force-skip-oidc | When set, the action will skip using GitHub OIDC provider even if the id-token permission is set. | No | | force-skip-oidc | When set, the action will skip using GitHub OIDC provider even if the id-token permission is set. | No |
| action-timeout-s | Global timeout for the action in seconds. If set to a value greater than 0, the action will fail if it takes longer than this time to complete. | No | | action-timeout-s | Global timeout for the action in seconds. If set to a value greater than 0, the action will fail if it takes longer than this time to complete. | No |
| no-proxy | Hosts to skip for the proxy configuration. | No |
| sts-endpoint | Custom STS endpoint URL. Use this to point to an STS-compatible API (e.g. MinIO, LocalStack) instead of the default AWS STS endpoint for the region. | No |
</details> </details>
@ -353,7 +350,8 @@ documentation for `GITHUB_` environment variable definitions][gh-env-vars])
[gh-env-vars]: [gh-env-vars]:
https://docs.github.com/en/actions/reference/workflows-and-actions/variables#default-environment-variables https://docs.github.com/en/actions/reference/workflows-and-actions/variables#default-environment-variables
**Default tags** are always emitted when session tags are used. **Protected tags** are always emitted when session tags are used, and cannot be
overridden via `custom-tags`:
| Key | Value | | Key | Value |
| ---------- | ----------------- | | ---------- | ----------------- |
@ -365,24 +363,21 @@ documentation for `GITHUB_` environment variable definitions][gh-env-vars])
| Commit | GITHUB_SHA | | Commit | GITHUB_SHA |
| Branch | GITHUB_REF | | Branch | GITHUB_REF |
**Droppable tags** are automatically added to the set of default session tags. **Overrideable tags** are automatically added to the set of default session tags
If the session tags exceed the [packed size limit][packed-size-limit], these but may be overridden via `custom-tags`. AWS has a maximum limit of 50 session
tags will be dropped, and the AssumeRole call will be retried. If it still tags; tags from this list are dropped in reverse priority order if your
fails, the action will error out. (It is difficult to predict the packed size `custom-tags` set plus the protected set exceeds this limit.
before making the call, as session tags and session policies are compressed into
a binary format as part of the call.)
[packed-size-limit]: | Key | Value | Priority |
https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html#id_session-tags_know | --------------- | ----------------------- | -------- |
| EventName | GITHUB_EVENT_NAME | 1 |
| Key | Value | | BaseRef | GITHUB_BASE_REF | 2 |
| --------------- | ----------------------- | | HeadRef | GITHUB_HEAD_REF | 3 |
| EventName | GITHUB_EVENT_NAME | | RefName | GITHUB_REF_NAME | 4 |
| BaseRef | GITHUB_BASE_REF | | RunId | GITHUB_RUN_ID | 5 |
| HeadRef | GITHUB_HEAD_REF | | RefType | GITHUB_REF_TYPE | 6 |
| RunId | GITHUB_RUN_ID | | Job | GITHUB_JOB | 7 |
| Job | GITHUB_JOB | | TriggeringActor | GITHUB_TRIGGERING_ACTOR | 8 |
| TriggeringActor | GITHUB_TRIGGERING_ACTOR |
Tags whose source environment variable is unset are omitted (e.g., `BaseRef` and Tags whose source environment variable is unset are omitted (e.g., `BaseRef` and
`HeadRef` are only set on `pull_request` events). `HeadRef` are only set on `pull_request` events).
@ -390,21 +385,21 @@ Tags whose source environment variable is unset are omitted (e.g., `BaseRef` and
_Note: all tag values must conform to _Note: all tag values must conform to
[the tag requirements][sts-tag-requirements]. [the tag requirements][sts-tag-requirements].
Values longer than 256 characters will be truncated, and characters outside the Values longer than 256 characters will be truncated, and characters outside the
allowed set will be replaced with an underscore (`_`)._ allowed set will be replaced with an underscore (`_`).\_
[sts-tag-requirements]: [sts-tag-requirements]:
https://docs.aws.amazon.com/STS/latest/APIReference/API_Tag.html https://docs.aws.amazon.com/STS/latest/APIReference/API_Tag.html
The action will use session tagging by default unless you are using OIDC or a The action will use session tagging by default unless you are using OIDC.
Web Identify Token File.
To [forward session tags to subsequent sessions in a role To [forward session tags to subsequent sessions in a role
chain][session-tag-chaining], you can use the `transitive-tag-keys` input to chain][session-tag-chaining], you can use
specify the keys of the tags to be passed.
[session-tag-chaining]: [session-tag-chaining]:
https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html#id_session-tags_role-chaining https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html#id_session-tags_role-chaining
the `transitive-tag-keys` input to specify the keys of the tags to be passed.
_Note that all subsequent roles in the chain must have _Note that all subsequent roles in the chain must have
`role-skip-session-tagging` set to `true`_ `role-skip-session-tagging` set to `true`_
@ -421,10 +416,9 @@ with:
### Custom session tags ### Custom session tags
You can add custom session tags using the `custom-tags` input, which accepts a You can add custom session tags using the `custom-tags` input, which accepts a
JSON object. Custom tags cannot override existing tags. Note that AWS allows a JSON object. Custom tags cannot override protected tags, but they can override
maximum of 50 tags (so you can supply a maximum of 43 custom tags), although it overrideable tags (in which case the overrideable tag's slot is freed for the
is likely that you will exceed the [packed size limit][packed-size-limit] next overrideable tag in the priority list, if any).
before you exceed the maximum number of tags.
```yaml ```yaml
uses: aws-actions/configure-aws-credentials@v6 uses: aws-actions/configure-aws-credentials@v6
@ -577,42 +571,42 @@ aws iam create-open-id-connect-provider \
### Claims and scoping permissions ### Claims and scoping permissions
To align with the Amazon IAM best practice of [granting least To align with the Amazon IAM best practice of [granting least
privilege][least-privilege], the assume role policy document should contain a privilege][least-privilege],
[`Condition`](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html)
that restricts which workflows can assume the role. Without any condition, any
GitHub user or repository could potentially assume the role.
GitHub provides a number of additional claims in the OIDC token that you can use
in your IAM policies to scope down permissions. Early versions of this action
only supported the `sub` and `aud` claims, but AWS IAM and GitHub have since
added support for `sub` claim customization and a variety of additional
claims ([1][gh-blog-oidc], [2][sub-claim-custom]).
> **Warning:** Avoid `ForAllValues:` in `Allow` statements. These operators
> return true when the claim is absent or misspelled, which can lead to
> unintended access. Instead, use `StringEquals` or `StringLike` operators to
> check for specific claim values.
[least-privilege]: [least-privilege]:
https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege
[gh-blog-oidc]:
https://aws.amazon.com/about-aws/whats-new/2026/01/aws-sts-supports-validation-identity-provider-claims/
[sub-claim-custom]:
https://docs.github.com/en/rest/actions/oidc?apiVersion=2026-03-10
#### Inspecting the token the assume role policy document should contain a
[`Condition`](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html)
that specifies a subject (`sub`) allowed to assume the role.
[GitHub also recommends](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#defining-trust-conditions-on-cloud-roles-using-oidc-claims)
filtering for the correct audience (`aud`). See
[AWS IAM documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_iam-condition-keys.html#condition-keys-wif)
on which claims you can filter for in your trust policies.
If you aren't sure what claim values your workflow is producing, the Without a subject (`sub`) condition, any GitHub user or repository could
potentially assume the role. The subject can be scoped to a GitHub organization
and repository as shown in the CloudFormation template. However, scoping it down
to your org and repo may cause the role assumption to fail in some cases. See
[Example subject claims](https://docs.github.com/en/actions/reference/security/oidc#example-subject-claims)
for specific details on what the subject value will be depending on your
workflow. You can also
[customize your subject claim](https://docs.github.com/en/actions/reference/security/oidc#customizing-the-token-claims)
if you want full control over the information you can filter for in your trust
policy. If you aren't sure what your subject (`sub`) key is, you can add the
[`actions-oidc-debugger`](https://github.com/github/actions-oidc-debugger) [`actions-oidc-debugger`](https://github.com/github/actions-oidc-debugger)
action will print the decoded JWT payload. Run it in a private repository action to your workflow to see the value of the subject (`sub`) key, as well as
only — the token itself is short-lived but the claim values may be sensitive. other claims.
See the GitHub [security-hardening guide][gh-oidc-hardening] for further Additional claim conditions can be added for higher specificity as explained in
discussion of trust conditions and threat modeling. the [GitHub documentation][gh-oidc-hardening].
[gh-oidc-hardening]: [gh-oidc-hardening]:
https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect
Due to implementation details, not every OIDC claim is presently supported by
IAM.
### Further information about OIDC ### Further information about OIDC
For further information on OIDC and GitHub Actions, please see: For further information on OIDC and GitHub Actions, please see:
@ -623,35 +617,6 @@ For further information on OIDC and GitHub Actions, please see:
- [GitHub docs: Configuring OpenID Connect in Amazon Web Services](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services) - [GitHub docs: Configuring OpenID Connect in Amazon Web Services](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services)
- [GitHub changelog: GitHub Actions: Secure cloud deployments with OpenID Connect](https://github.blog/changelog/2021-10-27-github-actions-secure-cloud-deployments-with-openid-connect/) - [GitHub changelog: GitHub Actions: Secure cloud deployments with OpenID Connect](https://github.blog/changelog/2021-10-27-github-actions-secure-cloud-deployments-with-openid-connect/)
## Getting Credentials in AWS Self-Hosted Runners
If you are running GitHub Actions in a self-hosted runner using an AWS Service
(such as Codebuild or EKS) and you have properly configured the service,
credentials should be available by default; the AWS CLI will fetch credentials
using the AWS_CONTAINER_CREDENTIALS_FULL_URI or
AWS_CONTAINER_CREDENTIALS_RELATIVE_URI environment variables. However, you may
still want to use this action if you need to export those credentials for use
with other tools in your workflow. You may also want to use this action in
scenarios where you need to use that 'default' role to assume another role.
To export credentials, simply run the action with `role-to-assume` set to the
default role of the container.
To assume another role from the container's default role, use the
`role-chaining: true` flag, so that the action fetches the default credentials
from the environment before assuming the other role.
If you are using EKS Pod Identities and encountering an error related to the
packed size of session tags, you must either run the action with
`role-skip-session-tagging: true` to disable the tags set by the action, or
[disable EKS session tagging][eks-disable-session-tagging] in the EKS settings
to disable the tags that are automatically set by the EKS Pod Identity Service.
Check the values of the action's session tags and the session tags that are
added by EKS so you can keep the set of tags which is more useful to you.
[eks-disable-session-tagging]:
https://docs.aws.amazon.com/eks/latest/userguide/pod-id-abac.html#pod-id-abac-tags
## Compatibility with non-GitHub Actions environments ## Compatibility with non-GitHub Actions environments
This action has been sucessfully tested with This action has been sucessfully tested with

View file

@ -642,11 +642,20 @@ Apache License
----------- -----------
The following npm package may be included in this product: The following npm packages may be included in this product:
- @aws-sdk/client-sts@3.1049.0 - @aws-sdk/client-sts@3.1045.0
- @aws-sdk/util-user-agent-browser@3.972.10
- @aws-sdk/util-user-agent-node@3.973.24
- @smithy/middleware-retry@4.5.7
- @smithy/querystring-builder@4.2.14
- @smithy/querystring-parser@4.2.14
- @smithy/service-error-classification@4.3.1
- @smithy/url-parser@4.2.14
- @smithy/util-defaults-mode-browser@4.3.49
- @smithy/util-defaults-mode-node@4.2.54
This package contains the following license: These packages each contain the following license:
Apache License Apache License
Version 2.0, January 2004 Version 2.0, January 2004
@ -854,9 +863,17 @@ Apache License
The following npm packages may be included in this product: The following npm packages may be included in this product:
- @aws-sdk/signature-v4-multi-region@3.996.27 - @aws-sdk/middleware-host-header@3.972.10
- @smithy/core@3.24.5 - @aws-sdk/middleware-recursion-detection@3.972.11
- @smithy/types@4.14.2 - @aws-sdk/middleware-sdk-s3@3.972.37
- @aws-sdk/middleware-user-agent@3.972.38
- @aws-sdk/signature-v4-multi-region@3.996.25
- @smithy/core@3.24.1
- @smithy/invalid-dependency@4.2.14
- @smithy/middleware-serde@4.2.20
- @smithy/protocol-http@5.3.14
- @smithy/smithy-client@4.12.13
- @smithy/types@4.14.1
These packages each contain the following license: These packages each contain the following license:
@ -1254,7 +1271,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
The following npm package may be included in this product: The following npm package may be included in this product:
- @aws-sdk/core@3.974.15 - @aws-sdk/core@3.974.8
This package contains the following license: This package contains the following license:
@ -1674,20 +1691,40 @@ Apache License
The following npm packages may be included in this product: The following npm packages may be included in this product:
- @aws-sdk/credential-provider-env@3.972.41 - @aws-sdk/credential-provider-env@3.972.34
- @aws-sdk/credential-provider-ini@3.972.42 - @aws-sdk/credential-provider-ini@3.972.38
- @aws-sdk/credential-provider-node@3.972.43 - @aws-sdk/credential-provider-node@3.972.39
- @aws-sdk/token-providers@3.1049.0 - @aws-sdk/region-config-resolver@3.972.13
- @aws-sdk/types@3.973.9 - @aws-sdk/token-providers@3.1041.0
- @aws-sdk/types@3.973.8
- @aws-sdk/util-arn-parser@3.972.3
- @aws-sdk/util-endpoints@3.996.8
- @aws-sdk/util-locate-window@3.965.5 - @aws-sdk/util-locate-window@3.965.5
- @aws-sdk/xml-builder@3.972.26 - @aws-sdk/xml-builder@3.972.22
- @smithy/credential-provider-imds@4.3.3 - @smithy/config-resolver@4.4.17
- @smithy/fetch-http-handler@5.4.3 - @smithy/credential-provider-imds@4.2.14
- @smithy/fetch-http-handler@5.3.17
- @smithy/hash-node@4.2.14
- @smithy/is-array-buffer@2.2.0 - @smithy/is-array-buffer@2.2.0
- @smithy/node-http-handler@4.7.3 - @smithy/is-array-buffer@4.2.2
- @smithy/signature-v4@5.4.5 - @smithy/middleware-content-length@4.2.14
- @smithy/middleware-endpoint@4.4.32
- @smithy/middleware-stack@4.2.14
- @smithy/node-http-handler@4.7.1
- @smithy/property-provider@4.3.1
- @smithy/shared-ini-file-loader@4.4.9
- @smithy/signature-v4@5.3.14
- @smithy/util-base64@4.3.2
- @smithy/util-body-length-browser@4.2.2
- @smithy/util-body-length-node@4.2.3
- @smithy/util-buffer-from@2.2.0 - @smithy/util-buffer-from@2.2.0
- @smithy/util-buffer-from@4.2.2
- @smithy/util-hex-encoding@4.2.2
- @smithy/util-stream@4.5.25
- @smithy/util-uri-escape@4.2.2
- @smithy/util-utf8@2.3.0 - @smithy/util-utf8@2.3.0
- @smithy/util-utf8@4.2.2
- @smithy/uuid@1.1.2
These packages each contain the following license: These packages each contain the following license:
@ -1897,9 +1934,9 @@ Apache License
The following npm packages may be included in this product: The following npm packages may be included in this product:
- @aws-sdk/credential-provider-process@3.972.38 - @aws-sdk/credential-provider-process@3.972.34
- @aws-sdk/credential-provider-sso@3.972.42 - @aws-sdk/credential-provider-sso@3.972.38
- @aws-sdk/credential-provider-web-identity@3.972.42 - @aws-sdk/credential-provider-web-identity@3.972.38
These packages each contain the following license: These packages each contain the following license:
@ -2109,9 +2146,433 @@ Apache License
The following npm packages may be included in this product: The following npm packages may be included in this product:
- @aws-sdk/credential-provider-http@3.972.40 - @aws-sdk/middleware-logger@3.972.10
- @aws-sdk/credential-provider-login@3.972.42 - @smithy/node-config-provider@4.3.14
- @aws-sdk/nested-clients@3.997.10 - @smithy/util-config-provider@4.2.2
These packages each contain the following license:
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-----------
The following npm packages may be included in this product:
- @smithy/util-endpoints@3.4.2
- @smithy/util-middleware@4.2.14
- @smithy/util-retry@4.3.8
These packages each contain the following license:
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-----------
The following npm packages may be included in this product:
- @aws-sdk/credential-provider-http@3.972.36
- @aws-sdk/credential-provider-login@3.972.38
- @aws-sdk/nested-clients@3.997.6
These packages each contain the following license: These packages each contain the following license:
@ -2335,9 +2796,8 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
The following npm packages may be included in this product: The following npm packages may be included in this product:
- @nodable/entities@2.1.1 - @nodable/entities@2.1.0
- quickjs-wasi@2.2.0 - quickjs-wasi@2.2.0
- xml-naming@0.1.0
These packages each contain the following license: These packages each contain the following license:
@ -2377,7 +2837,7 @@ SOFTWARE.
The following npm package may be included in this product: The following npm package may be included in this product:
- fast-xml-parser@5.7.3 - fast-xml-parser@5.7.2
This package contains the following license: This package contains the following license:
@ -2407,7 +2867,7 @@ SOFTWARE.
The following npm package may be included in this product: The following npm package may be included in this product:
- strnum@2.3.0 - strnum@2.2.3
This package contains the following license: This package contains the following license:
@ -2467,7 +2927,7 @@ SOFTWARE.
The following npm package may be included in this product: The following npm package may be included in this product:
- fast-xml-builder@1.2.0 - fast-xml-builder@1.1.9
This package contains the following license: This package contains the following license:

24
dist/cleanup/index.js generated vendored
View file

@ -5707,7 +5707,7 @@ var require_client_h1 = __commonJS({
kResume, kResume,
kHTTPContext kHTTPContext
} = require_symbols(); } = require_symbols();
var constants4 = require_constants2(); var constants3 = require_constants2();
var EMPTY_BUF = Buffer.alloc(0); var EMPTY_BUF = Buffer.alloc(0);
var FastBuffer = Buffer[Symbol.species]; var FastBuffer = Buffer[Symbol.species];
var addListener = util.addListener; var addListener = util.addListener;
@ -5779,7 +5779,7 @@ var require_client_h1 = __commonJS({
constructor(client, socket, { exports: exports3 }) { constructor(client, socket, { exports: exports3 }) {
assert(Number.isFinite(client[kMaxHeadersSize]) && client[kMaxHeadersSize] > 0); assert(Number.isFinite(client[kMaxHeadersSize]) && client[kMaxHeadersSize] > 0);
this.llhttp = exports3; this.llhttp = exports3;
this.ptr = this.llhttp.llhttp_alloc(constants4.TYPE.RESPONSE); this.ptr = this.llhttp.llhttp_alloc(constants3.TYPE.RESPONSE);
this.client = client; this.client = client;
this.socket = socket; this.socket = socket;
this.timeout = null; this.timeout = null;
@ -5874,19 +5874,19 @@ var require_client_h1 = __commonJS({
currentBufferRef = null; currentBufferRef = null;
} }
const offset = llhttp.llhttp_get_error_pos(this.ptr) - currentBufferPtr; const offset = llhttp.llhttp_get_error_pos(this.ptr) - currentBufferPtr;
if (ret === constants4.ERROR.PAUSED_UPGRADE) { if (ret === constants3.ERROR.PAUSED_UPGRADE) {
this.onUpgrade(data.slice(offset)); this.onUpgrade(data.slice(offset));
} else if (ret === constants4.ERROR.PAUSED) { } else if (ret === constants3.ERROR.PAUSED) {
this.paused = true; this.paused = true;
socket.unshift(data.slice(offset)); socket.unshift(data.slice(offset));
} else if (ret !== constants4.ERROR.OK) { } else if (ret !== constants3.ERROR.OK) {
const ptr = llhttp.llhttp_get_error_reason(this.ptr); const ptr = llhttp.llhttp_get_error_reason(this.ptr);
let message = ""; let message = "";
if (ptr) { if (ptr) {
const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0); const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0);
message = "Response does not match the HTTP/1.1 protocol (" + Buffer.from(llhttp.memory.buffer, ptr, len).toString() + ")"; message = "Response does not match the HTTP/1.1 protocol (" + Buffer.from(llhttp.memory.buffer, ptr, len).toString() + ")";
} }
throw new HTTPParserError(message, constants4.ERROR[ret], data.slice(offset)); throw new HTTPParserError(message, constants3.ERROR[ret], data.slice(offset));
} }
} catch (err) { } catch (err) {
util.destroy(socket, err); util.destroy(socket, err);
@ -6061,7 +6061,7 @@ var require_client_h1 = __commonJS({
socket[kBlocking] = false; socket[kBlocking] = false;
client[kResume](); client[kResume]();
} }
return pause ? constants4.ERROR.PAUSED : 0; return pause ? constants3.ERROR.PAUSED : 0;
} }
onBody(buf) { onBody(buf) {
const { client, socket, statusCode, maxResponseSize } = this; const { client, socket, statusCode, maxResponseSize } = this;
@ -6083,7 +6083,7 @@ var require_client_h1 = __commonJS({
} }
this.bytesRead += buf.length; this.bytesRead += buf.length;
if (request.onData(buf) === false) { if (request.onData(buf) === false) {
return constants4.ERROR.PAUSED; return constants3.ERROR.PAUSED;
} }
} }
onMessageComplete() { onMessageComplete() {
@ -6118,13 +6118,13 @@ var require_client_h1 = __commonJS({
if (socket[kWriting]) { if (socket[kWriting]) {
assert(client[kRunning] === 0); assert(client[kRunning] === 0);
util.destroy(socket, new InformationalError("reset")); util.destroy(socket, new InformationalError("reset"));
return constants4.ERROR.PAUSED; return constants3.ERROR.PAUSED;
} else if (!shouldKeepAlive) { } else if (!shouldKeepAlive) {
util.destroy(socket, new InformationalError("reset")); util.destroy(socket, new InformationalError("reset"));
return constants4.ERROR.PAUSED; return constants3.ERROR.PAUSED;
} else if (socket[kReset] && client[kRunning] === 0) { } else if (socket[kReset] && client[kRunning] === 0) {
util.destroy(socket, new InformationalError("reset")); util.destroy(socket, new InformationalError("reset"));
return constants4.ERROR.PAUSED; return constants3.ERROR.PAUSED;
} else if (client[kPipelining] == null || client[kPipelining] === 1) { } else if (client[kPipelining] == null || client[kPipelining] === 1) {
setImmediate(() => client[kResume]()); setImmediate(() => client[kResume]());
} else { } else {
@ -19128,7 +19128,6 @@ function error(message, properties = {}) {
} }
// src/helpers.ts // src/helpers.ts
var fs3 = __toESM(require("node:fs"));
function errorMessage(error2) { function errorMessage(error2) {
return error2 instanceof Error ? error2.message : String(error2); return error2 instanceof Error ? error2.message : String(error2);
} }
@ -19146,7 +19145,6 @@ function getBooleanInput(name, options) {
Support boolean input list: \`true | True | TRUE | false | False | FALSE\`` Support boolean input list: \`true | True | TRUE | false | False | FALSE\``
); );
} }
var O_NOFOLLOW = fs3.constants.O_NOFOLLOW ?? 0;
// src/cleanup/index.ts // src/cleanup/index.ts
function cleanup() { function cleanup() {

21105
dist/index.js generated vendored

File diff suppressed because one or more lines are too long

1453
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
{ {
"name": "configure-aws-credentials", "name": "configure-aws-credentials",
"description": "A GitHub Action to configure AWS credentials", "description": "A GitHub Action to configure AWS credentials",
"version": "6.2.0", "version": "6.1.1",
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"lint": "biome check --error-on-warnings ./src ./test && markdownlint -i node_modules -i CHANGELOG.md '**/*.md'", "lint": "biome check --error-on-warnings ./src ./test && markdownlint -i node_modules -i CHANGELOG.md '**/*.md'",
@ -17,11 +17,11 @@
"organization": true "organization": true
}, },
"devDependencies": { "devDependencies": {
"@aws-sdk/credential-provider-env": "^3.972.39", "@aws-sdk/credential-provider-env": "^3.972.32",
"@biomejs/biome": "2.4.15", "@biomejs/biome": "2.4.15",
"@smithy/property-provider": "^4.3.4", "@smithy/property-provider": "^4.3.1",
"@types/node": "^25.9.1", "@types/node": "^25.7.0",
"@vitest/coverage-v8": "4.1.5", "@vitest/coverage-v8": "^4.1.6",
"aws-sdk-client-mock": "^4.1.0", "aws-sdk-client-mock": "^4.1.0",
"esbuild": "^0.28.0", "esbuild": "^0.28.0",
"generate-license-file": "^4.1.1", "generate-license-file": "^4.1.1",
@ -30,12 +30,12 @@
"memfs": "^4.57.2", "memfs": "^4.57.2",
"standard-version": "^9.5.0", "standard-version": "^9.5.0",
"typescript": "^6.0.3", "typescript": "^6.0.3",
"vitest": "4.1.5" "vitest": "^4.1.5"
}, },
"dependencies": { "dependencies": {
"@actions/core": "^3.0.1", "@actions/core": "^3.0.1",
"@aws-sdk/client-sts": "^3.1049.0", "@aws-sdk/client-sts": "^3.1045.0",
"@smithy/node-http-handler": "^4.7.3", "@smithy/node-http-handler": "^4.7.1",
"proxy-agent": "^8.0.1" "proxy-agent": "^8.0.1"
}, },
"keywords": [ "keywords": [

View file

@ -1,14 +1,11 @@
import assert from 'node:assert'; import assert from 'node:assert';
import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import * as core from '@actions/core'; import * as core from '@actions/core';
import type { AssumeRoleCommandInput, STSClient, Tag } from '@aws-sdk/client-sts'; import type { AssumeRoleCommandInput, STSClient, Tag } from '@aws-sdk/client-sts';
import { import { AssumeRoleCommand, AssumeRoleWithWebIdentityCommand } from '@aws-sdk/client-sts';
AssumeRoleCommand,
AssumeRoleWithWebIdentityCommand,
PackedPolicyTooLargeException,
} from '@aws-sdk/client-sts';
import type { CredentialsClient } from './CredentialsClient'; import type { CredentialsClient } from './CredentialsClient';
import { errorMessage, isDefined, readFileUtf8, sanitizeGitHubVariables } from './helpers'; import { errorMessage, isDefined, sanitizeGitHubVariables } from './helpers';
async function assumeRoleWithOIDC(params: AssumeRoleCommandInput, client: STSClient, webIdentityToken: string) { async function assumeRoleWithOIDC(params: AssumeRoleCommandInput, client: STSClient, webIdentityToken: string) {
delete params.Tags; delete params.Tags;
@ -39,14 +36,13 @@ async function assumeRoleWithWebIdentityTokenFile(
const webIdentityTokenFilePath = path.isAbsolute(webIdentityTokenFile) const webIdentityTokenFilePath = path.isAbsolute(webIdentityTokenFile)
? webIdentityTokenFile ? webIdentityTokenFile
: path.join(workspace, webIdentityTokenFile); : path.join(workspace, webIdentityTokenFile);
const webIdentityToken = readFileUtf8(webIdentityTokenFilePath); if (!fs.existsSync(webIdentityTokenFilePath)) {
if (webIdentityToken === null) {
throw new Error(`Web identity token file does not exist: ${webIdentityTokenFilePath}`); throw new Error(`Web identity token file does not exist: ${webIdentityTokenFilePath}`);
} }
core.info('Assuming role with web identity token file'); core.info('Assuming role with web identity token file');
try { try {
const webIdentityToken = fs.readFileSync(webIdentityTokenFilePath, 'utf8');
delete params.Tags; delete params.Tags;
delete params.TransitiveTagKeys;
const creds = await client.send( const creds = await client.send(
new AssumeRoleWithWebIdentityCommand({ new AssumeRoleWithWebIdentityCommand({
...params, ...params,
@ -65,13 +61,6 @@ async function assumeRoleWithCredentials(params: AssumeRoleCommandInput, client:
const creds = await client.send(new AssumeRoleCommand({ ...params })); const creds = await client.send(new AssumeRoleCommand({ ...params }));
return creds; return creds;
} catch (error) { } catch (error) {
if (error instanceof PackedPolicyTooLargeException) {
core.info('Session tag size is too large; dropping droppable tags and retrying.');
const droppableKeys = new Set(DROPPABLE_TAG_SOURCES.map((s) => s.key));
params.Tags = params.Tags?.filter((tag) => !droppableKeys.has(tag.Key ?? ''));
const creds = await client.send(new AssumeRoleCommand({ ...params }));
return creds;
}
throw new Error(`Could not assume role with user credentials: ${errorMessage(error)}`); throw new Error(`Could not assume role with user credentials: ${errorMessage(error)}`);
} }
} }
@ -98,8 +87,8 @@ const MAX_TAG_KEY_LENGTH = 128;
const MAX_TAG_VALUE_LENGTH = 256; const MAX_TAG_VALUE_LENGTH = 256;
const MAX_SESSION_TAGS = 50; const MAX_SESSION_TAGS = 50;
// Identity/audit primitives. Always emitted and cannot be dropped. // Identity/audit primitives. Always emitted and cannot be overridden by custom-tags.
const NON_DROPPABLE_TAG_SOURCES: ReadonlyArray<{ key: string; envVar: string }> = [ const PROTECTED_TAG_SOURCES: ReadonlyArray<{ key: string; envVar: string }> = [
{ key: 'Repository', envVar: 'GITHUB_REPOSITORY' }, { key: 'Repository', envVar: 'GITHUB_REPOSITORY' },
{ key: 'Workflow', envVar: 'GITHUB_WORKFLOW' }, { key: 'Workflow', envVar: 'GITHUB_WORKFLOW' },
{ key: 'Action', envVar: 'GITHUB_ACTION' }, { key: 'Action', envVar: 'GITHUB_ACTION' },
@ -108,22 +97,21 @@ const NON_DROPPABLE_TAG_SOURCES: ReadonlyArray<{ key: string; envVar: string }>
{ key: 'Branch', envVar: 'GITHUB_REF' }, { key: 'Branch', envVar: 'GITHUB_REF' },
]; ];
// Convenience metadata. If the AssumeRole call fails due to compressed size of // Convenience metadata. Custom-tags may override (suppresses the default for that key).
// session tags being too large, we will drop these tags and retry once. // Listed in priority order; lower-priority entries are dropped first if the user's custom-tags
const DROPPABLE_TAG_SOURCES: ReadonlyArray<{ key: string; envVar: string }> = [ // would push the total above MAX_SESSION_TAGS.
const OVERRIDEABLE_TAG_SOURCES_BY_PRIORITY: ReadonlyArray<{ key: string; envVar: string }> = [
{ key: 'EventName', envVar: 'GITHUB_EVENT_NAME' }, { key: 'EventName', envVar: 'GITHUB_EVENT_NAME' },
{ key: 'BaseRef', envVar: 'GITHUB_BASE_REF' }, { key: 'BaseRef', envVar: 'GITHUB_BASE_REF' },
{ key: 'HeadRef', envVar: 'GITHUB_HEAD_REF' }, { key: 'HeadRef', envVar: 'GITHUB_HEAD_REF' },
{ key: 'RefName', envVar: 'GITHUB_REF_NAME' },
{ key: 'RunId', envVar: 'GITHUB_RUN_ID' }, { key: 'RunId', envVar: 'GITHUB_RUN_ID' },
{ key: 'RefType', envVar: 'GITHUB_REF_TYPE' },
{ key: 'Job', envVar: 'GITHUB_JOB' }, { key: 'Job', envVar: 'GITHUB_JOB' },
{ key: 'TriggeringActor', envVar: 'GITHUB_TRIGGERING_ACTOR' }, { key: 'TriggeringActor', envVar: 'GITHUB_TRIGGERING_ACTOR' },
]; ];
const PROTECTED_TAG_KEYS = new Set<string>([ const PROTECTED_TAG_KEYS = new Set<string>(['GitHub', ...PROTECTED_TAG_SOURCES.map((s) => s.key)]);
'GitHub',
...NON_DROPPABLE_TAG_SOURCES.map((s) => s.key),
...DROPPABLE_TAG_SOURCES.map((s) => s.key),
]);
export function parseAndValidateCustomTags(customTags: string, existingTags: Tag[]): Tag[] { export function parseAndValidateCustomTags(customTags: string, existingTags: Tag[]): Tag[] {
let parsed: unknown; let parsed: unknown;
@ -210,13 +198,7 @@ export async function assumeRole(params: assumeRoleParams) {
// Build session tags. Values are sanitized because the AWS tag value spec is more // Build session tags. Values are sanitized because the AWS tag value spec is more
// restrictive than permissible characters in environment variables. // restrictive than permissible characters in environment variables.
const protectedTags: Tag[] = [{ Key: 'GitHub', Value: 'Actions' }]; const protectedTags: Tag[] = [{ Key: 'GitHub', Value: 'Actions' }];
for (const { key, envVar } of NON_DROPPABLE_TAG_SOURCES) { for (const { key, envVar } of PROTECTED_TAG_SOURCES) {
const value = process.env[envVar];
if (value) {
protectedTags.push({ Key: key, Value: sanitizeGitHubVariables(value) });
}
}
for (const { key, envVar } of DROPPABLE_TAG_SOURCES) {
const value = process.env[envVar]; const value = process.env[envVar];
if (value) { if (value) {
protectedTags.push({ Key: key, Value: sanitizeGitHubVariables(value) }); protectedTags.push({ Key: key, Value: sanitizeGitHubVariables(value) });
@ -224,15 +206,26 @@ export async function assumeRole(params: assumeRoleParams) {
} }
const parsedCustomTags: Tag[] = customTags ? parseAndValidateCustomTags(customTags, protectedTags) : []; const parsedCustomTags: Tag[] = customTags ? parseAndValidateCustomTags(customTags, protectedTags) : [];
const customTagKeys = new Set(parsedCustomTags.map((t) => t.Key));
const tagArray: Tag[] = [...protectedTags, ...parsedCustomTags]; const availableOverrideableSlots = MAX_SESSION_TAGS - protectedTags.length - parsedCustomTags.length;
const overrideableTags: Tag[] = [];
for (const { key, envVar } of OVERRIDEABLE_TAG_SOURCES_BY_PRIORITY) {
if (overrideableTags.length >= availableOverrideableSlots) break;
if (customTagKeys.has(key)) continue;
const value = process.env[envVar];
if (value) {
overrideableTags.push({ Key: key, Value: sanitizeGitHubVariables(value) });
}
}
const tagArray: Tag[] = [...protectedTags, ...overrideableTags, ...parsedCustomTags];
const tags = roleSkipSessionTagging ? undefined : tagArray; const tags = roleSkipSessionTagging ? undefined : tagArray;
if (!tags) { if (!tags) {
core.debug('Role session tagging has been skipped.'); core.debug('Role session tagging has been skipped.');
} else { } else {
core.debug(`${tags.length} role session tags are being used:`); core.debug(`${tags.length} role session tags are being used:`);
core.debug(JSON.stringify(tagArray));
} }
//only populate transitiveTagKeys array if user is actually using session tagging //only populate transitiveTagKeys array if user is actually using session tagging

View file

@ -1,5 +1,3 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as core from '@actions/core'; import * as core from '@actions/core';
import type { Credentials, STSClient } from '@aws-sdk/client-sts'; import type { Credentials, STSClient } from '@aws-sdk/client-sts';
import { GetCallerIdentityCommand } from '@aws-sdk/client-sts'; import { GetCallerIdentityCommand } from '@aws-sdk/client-sts';
@ -293,106 +291,3 @@ export function getBooleanInput(name: string, options?: core.InputOptions & { de
`Support boolean input list: \`true | True | TRUE | false | False | FALSE\``, `Support boolean input list: \`true | True | TRUE | false | False | FALSE\``,
); );
} }
// O_NOFOLLOW is undefined on Windows. This sets it to 0 if it's not defined.
const O_NOFOLLOW: number = (fs.constants as { O_NOFOLLOW?: number }).O_NOFOLLOW ?? 0;
export function isAllowListed(filePath: string): boolean {
// Kubelet projects service-account tokens through a symlink chain
// (token -> ..data/token, ..data -> ..<timestamp>/). The containing path is
// kubelet-controlled, so we allow symlink-following reads of this fixed
// location only.
const KUBERNETES_TOKEN_PATH_REGEX = /^\/var\/run\/secrets\/[^/]+\/serviceaccount\/token$/;
if (process.platform !== 'win32') {
// No Kubernetes token paths on Windows
return KUBERNETES_TOKEN_PATH_REGEX.test(path.posix.normalize(filePath));
}
return false;
}
export function isSymlink(filePath: string): boolean {
try {
return fs.lstatSync(filePath).isSymbolicLink();
} catch (err) {
if ((err as NodeJS.ErrnoException).code === 'ENOENT') return false;
throw err;
}
}
// Refuses if filePath or its parent directory is a symbolic link.
function refuseSymlinkOnPath(filePath: string): void {
const parent = path.dirname(filePath);
if (parent !== filePath && isSymlink(parent)) {
throw new Error(`Refusing ${filePath} (parent directory is a symbolic link)`);
}
if (isSymlink(filePath)) {
throw new Error(`Refusing ${filePath} (path is a symbolic link)`);
}
}
function assertRegularFile(fd: number, filePath: string): void {
const stats = fs.fstatSync(fd);
if (!stats.isFile()) {
throw new Error(`${filePath} (path is not a regular file)`);
}
}
// ENOENT: file does not exist
// ELOOP: too many symbolic links (from NOFOLLOW)
export function readFileUtf8(filePath: string): string | null {
const allowSymlink = isAllowListed(filePath);
if (!allowSymlink) {
refuseSymlinkOnPath(filePath);
}
const openFlags = fs.constants.O_RDONLY | (allowSymlink ? 0 : O_NOFOLLOW);
let fd: number;
try {
fd = fs.openSync(filePath, openFlags);
} catch (err) {
const code = (err as NodeJS.ErrnoException).code;
if (code === 'ENOENT') return null;
if (code === 'ELOOP') {
throw new Error(`Refusing ${filePath} (path is a symbolic link)`);
}
throw err;
}
try {
assertRegularFile(fd, filePath);
return fs.readFileSync(fd, 'utf-8');
} finally {
fs.closeSync(fd);
}
}
export function writeFileUtf8(filePath: string, content: string, mode = 0o600): void {
refuseSymlinkOnPath(filePath);
let fd: number;
try {
fd = fs.openSync(filePath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC | O_NOFOLLOW, mode);
} catch (err) {
if ((err as NodeJS.ErrnoException).code === 'ELOOP') {
throw new Error(`Refusing ${filePath} (path is a symbolic link)`);
}
throw err;
}
try {
assertRegularFile(fd, filePath);
// openSync only applies mode on creation.
// If the file already exists, we need to ensure the mode is correct.
if (process.platform !== 'win32') {
fs.fchmodSync(fd, mode);
}
fs.writeFileSync(fd, content);
} finally {
fs.closeSync(fd);
}
}
export function mkdir(dir: string, mode = 0o700): void {
fs.mkdirSync(dir, { recursive: true, mode });
if (isSymlink(dir)) {
throw new Error(`Refusing ${dir} (path is a symbolic link)`);
}
}

View file

@ -1,8 +1,8 @@
import * as fs from 'node:fs';
import * as os from 'node:os'; import * as os from 'node:os';
import * as path from 'node:path'; import * as path from 'node:path';
import * as core from '@actions/core'; import * as core from '@actions/core';
import type { Credentials } from '@aws-sdk/client-sts'; import type { Credentials } from '@aws-sdk/client-sts';
import { mkdir, readFileUtf8, writeFileUtf8 } from './helpers';
/** /**
* Parse an INI-format string into a nested object. * Parse an INI-format string into a nested object.
@ -87,8 +87,10 @@ export function getProfileFilePaths(): ProfileFilePaths {
*/ */
export function ensureAwsDirectoryExists(filePath: string): void { export function ensureAwsDirectoryExists(filePath: string): void {
const dir = path.dirname(filePath); const dir = path.dirname(filePath);
core.debug(`Ensuring directory exists: ${dir}`); if (!fs.existsSync(dir)) {
mkdir(dir, 0o700); core.debug(`Creating directory: ${dir}`);
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
}
} }
/** /**
@ -125,8 +127,14 @@ export function mergeProfileSection(
data: Record<string, string>, data: Record<string, string>,
overwriteAwsProfile: boolean, overwriteAwsProfile: boolean,
): void { ): void {
const fileContent = readFileUtf8(filePath); let existingContent: Record<string, Record<string, string>> = {};
const existingContent: Record<string, Record<string, string>> = fileContent === null ? {} : parseIni(fileContent);
// Read existing file if it exists
if (fs.existsSync(filePath)) {
core.debug(`Reading existing file: ${filePath}`);
const fileContent = fs.readFileSync(filePath, 'utf-8');
existingContent = parseIni(fileContent);
}
if (existingContent[sectionName] && !overwriteAwsProfile) { if (existingContent[sectionName] && !overwriteAwsProfile) {
throw new Error( throw new Error(
@ -139,7 +147,7 @@ export function mergeProfileSection(
const content = stringifyIni(existingContent); const content = stringifyIni(existingContent);
core.debug(`Writing profile to ${filePath}`); core.debug(`Writing profile to ${filePath}`);
writeFileUtf8(filePath, content, 0o600); fs.writeFileSync(filePath, content, { mode: 0o600 });
} }
/** /**

View file

@ -1,63 +0,0 @@
import * as core from '@actions/core';
import { AssumeRoleWithWebIdentityCommand, GetCallerIdentityCommand, STSClient } from '@aws-sdk/client-sts';
import { mockClient } from 'aws-sdk-client-mock';
import { fs, vol } from 'memfs';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import * as helpers from '../src/helpers';
import { run } from '../src/index';
import mocks from './mockinputs.test';
vi.mock('node:fs');
vi.mock('@actions/core');
const mockedSTSClient = mockClient(STSClient);
describe('assumeRoleWithWebIdentityTokenFile', {}, () => {
beforeEach(() => {
vi.restoreAllMocks();
vi.clearAllMocks();
mockedSTSClient.reset();
vol.reset();
helpers.withsleep(() => Promise.resolve());
vi.mocked(core.getInput).mockImplementation(mocks.getInput(mocks.WEBIDENTITY_TOKEN_FILE_INPUTS));
vi.mocked(core.getMultilineInput).mockReturnValue([]);
mockedSTSClient.on(GetCallerIdentityCommand).resolves({ ...mocks.outputs.GET_CALLER_IDENTITY });
process.env = { ...mocks.envs };
fs.mkdirSync('/home/github', { recursive: true });
});
afterEach(() => {
helpers.reset();
});
it('refuses when the token file is a symlink and never calls STS', async () => {
fs.mkdirSync('/etc', { recursive: true });
fs.writeFileSync('/etc/passwd', 'root:x:0:0::/root:/bin/sh');
fs.symlinkSync('/etc/passwd', '/home/github/file.txt');
await run();
expect(core.setFailed).toHaveBeenCalledWith(expect.stringMatching(/Refusing .* \(.* symbolic link\)/));
expect(mockedSTSClient.commandCalls(AssumeRoleWithWebIdentityCommand)).toHaveLength(0);
expect(fs.readFileSync('/etc/passwd', 'utf-8')).toBe('root:x:0:0::/root:/bin/sh');
});
it('preserves the existing missing-file error when the token file does not exist', async () => {
await run();
expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining('Web identity token file does not exist'));
expect(mockedSTSClient.commandCalls(AssumeRoleWithWebIdentityCommand)).toHaveLength(0);
});
it('passes token contents to STS when the file is regular', async () => {
fs.writeFileSync('/home/github/file.txt', 'real-token');
mockedSTSClient.on(AssumeRoleWithWebIdentityCommand).resolves(mocks.outputs.STS_CREDENTIALS);
await run();
expect(core.setFailed).not.toHaveBeenCalled();
const calls = mockedSTSClient.commandCalls(AssumeRoleWithWebIdentityCommand);
expect(calls).toHaveLength(1);
expect(calls[0]?.args[0].input.WebIdentityToken).toBe('real-token');
});
});

View file

@ -1,16 +1,12 @@
import * as core from '@actions/core'; import * as core from '@actions/core';
import { fs, vol } from 'memfs';
import { beforeEach, describe, expect, it, vi } from 'vitest'; import { beforeEach, describe, expect, it, vi } from 'vitest';
import * as helpers from '../src/helpers'; import * as helpers from '../src/helpers';
vi.mock('node:fs');
vi.mock('@actions/core'); vi.mock('@actions/core');
describe('Configure AWS Credentials helpers', {}, () => { describe('Configure AWS Credentials helpers', {}, () => {
beforeEach(() => { beforeEach(() => {
vi.restoreAllMocks(); vi.resetAllMocks();
vi.clearAllMocks();
vol.reset();
}); });
it('removes brackets from GitHub Actor', {}, () => { it('removes brackets from GitHub Actor', {}, () => {
const actor = 'actor[bot]'; const actor = 'actor[bot]';
@ -52,9 +48,6 @@ describe('Configure AWS Credentials helpers', {}, () => {
helpers.reset(); helpers.reset();
}); });
it('can output creds when told to', {}, () => { it('can output creds when told to', {}, () => {
vi.spyOn(core, 'setOutput').mockImplementation(() => {});
vi.spyOn(core, 'setSecret').mockImplementation(() => {});
vi.spyOn(core, 'exportVariable').mockImplementation(() => {});
helpers.exportCredentials( helpers.exportCredentials(
{ AccessKeyId: 'test', SecretAccessKey: 'test', SessionToken: 'test', Expiration: new Date(8640000000000000) }, { AccessKeyId: 'test', SecretAccessKey: 'test', SessionToken: 'test', Expiration: new Date(8640000000000000) },
true, true,
@ -75,9 +68,6 @@ describe('Configure AWS Credentials helpers', {}, () => {
process.env = env; process.env = env;
}); });
it(`won't output credentials to env if told not to`, {}, () => { it(`won't output credentials to env if told not to`, {}, () => {
vi.spyOn(core, 'setOutput').mockImplementation(() => {});
vi.spyOn(core, 'setSecret').mockImplementation(() => {});
vi.spyOn(core, 'exportVariable').mockImplementation(() => {});
helpers.exportCredentials( helpers.exportCredentials(
{ AccessKeyId: 'test', SecretAccessKey: 'test', SessionToken: 'test', Expiration: new Date(8640000000000000) }, { AccessKeyId: 'test', SecretAccessKey: 'test', SessionToken: 'test', Expiration: new Date(8640000000000000) },
true, true,
@ -105,163 +95,22 @@ describe('Configure AWS Credentials helpers', {}, () => {
}); });
it('handles getBooleanInput correctly', {}, () => { it('handles getBooleanInput correctly', {}, () => {
vi.spyOn(core, 'getInput').mockReturnValue('true'); vi.mocked(core.getInput).mockReturnValue('true');
expect(helpers.getBooleanInput('test')).toBe(true); expect(helpers.getBooleanInput('test')).toBe(true);
vi.spyOn(core, 'getInput').mockReturnValue('false'); vi.mocked(core.getInput).mockReturnValue('false');
expect(helpers.getBooleanInput('test')).toBe(false); expect(helpers.getBooleanInput('test')).toBe(false);
vi.spyOn(core, 'getInput').mockReturnValue(''); vi.mocked(core.getInput).mockReturnValue('');
expect(helpers.getBooleanInput('test', { default: true })).toBe(true); expect(helpers.getBooleanInput('test', { default: true })).toBe(true);
vi.spyOn(core, 'getInput').mockReturnValue('invalid'); vi.mocked(core.getInput).mockReturnValue('invalid');
expect(() => helpers.getBooleanInput('test')).toThrow(); expect(() => helpers.getBooleanInput('test')).toThrow();
}); });
it('clears session token when not provided', {}, () => { it('clears session token when not provided', {}, () => {
vi.spyOn(core, 'setSecret').mockImplementation(() => {});
vi.spyOn(core, 'exportVariable').mockImplementation(() => {});
process.env.AWS_SESSION_TOKEN = 'old-token'; process.env.AWS_SESSION_TOKEN = 'old-token';
helpers.exportCredentials({ AccessKeyId: 'test', SecretAccessKey: 'test' }, false, true); helpers.exportCredentials({ AccessKeyId: 'test', SecretAccessKey: 'test' }, false, true);
expect(core.exportVariable).toHaveBeenCalledWith('AWS_SESSION_TOKEN', ''); expect(core.exportVariable).toHaveBeenCalledWith('AWS_SESSION_TOKEN', '');
}); });
describe('filesystem helpers', {}, () => {
describe('isSymlink', {}, () => {
it('returns true for a symlink', {}, () => {
fs.mkdirSync('/dir', { recursive: true });
fs.writeFileSync('/dir/target', 'data');
fs.symlinkSync('/dir/target', '/dir/link');
expect(helpers.isSymlink('/dir/link')).toBe(true);
});
it('returns false for a regular file', {}, () => {
fs.mkdirSync('/dir', { recursive: true });
fs.writeFileSync('/dir/file', 'data');
expect(helpers.isSymlink('/dir/file')).toBe(false);
});
it('returns false for a missing path', {}, () => {
expect(helpers.isSymlink('/nonexistent')).toBe(false);
});
});
describe('readFileUtf8', {}, () => {
it('returns content for a regular file', {}, () => {
fs.mkdirSync('/dir', { recursive: true });
fs.writeFileSync('/dir/file', 'hello');
expect(helpers.readFileUtf8('/dir/file')).toBe('hello');
});
it('returns null when the file does not exist', {}, () => {
fs.mkdirSync('/dir', { recursive: true });
expect(helpers.readFileUtf8('/dir/missing')).toBe(null);
});
it('refuses to read through a symlink at the target', {}, () => {
fs.mkdirSync('/dir', { recursive: true });
fs.writeFileSync('/dir/secret', 'sensitive');
fs.symlinkSync('/dir/secret', '/dir/link');
expect(() => helpers.readFileUtf8('/dir/link')).toThrow(/Refusing .* \(.* symbolic link\)/);
});
it('refuses to read when the parent directory is a symlink', {}, () => {
fs.mkdirSync('/real/.aws', { recursive: true });
fs.writeFileSync('/real/.aws/credentials', 'data');
fs.mkdirSync('/home', { recursive: true });
fs.symlinkSync('/real/.aws', '/home/.aws');
expect(() => helpers.readFileUtf8('/home/.aws/credentials')).toThrow(/Refusing .* \(.* symbolic link\)/);
});
it('refuses to read when the path is a directory', {}, () => {
fs.mkdirSync('/dir/subdir', { recursive: true });
expect(() => helpers.readFileUtf8('/dir/subdir')).toThrow(/not a regular file/);
});
it.skipIf(process.platform === 'win32')(
'follows the kubelet projected-token symlink chain at /var/run/secrets/*/serviceaccount/token',
() => {
fs.mkdirSync('/var/run/secrets/eks.amazonaws.com/serviceaccount/..2026_05_28_00_00_00.123', {
recursive: true,
});
fs.writeFileSync(
'/var/run/secrets/eks.amazonaws.com/serviceaccount/..2026_05_28_00_00_00.123/token',
'jwt-token',
);
fs.symlinkSync('..2026_05_28_00_00_00.123', '/var/run/secrets/eks.amazonaws.com/serviceaccount/..data');
fs.symlinkSync('..data/token', '/var/run/secrets/eks.amazonaws.com/serviceaccount/token');
expect(helpers.readFileUtf8('/var/run/secrets/eks.amazonaws.com/serviceaccount/token')).toBe('jwt-token');
},
);
it.skipIf(process.platform === 'win32')('still refuses symlinks at lookalike paths outside the allowlist', () => {
fs.mkdirSync('/var/run/secrets/eks.amazonaws.com/serviceaccount', { recursive: true });
fs.writeFileSync('/var/run/secrets/eks.amazonaws.com/serviceaccount/secret', 'jwt-token');
fs.symlinkSync(
'/var/run/secrets/eks.amazonaws.com/serviceaccount/secret',
'/var/run/secrets/eks.amazonaws.com/serviceaccount/token2',
);
expect(() => helpers.readFileUtf8('/var/run/secrets/eks.amazonaws.com/serviceaccount/token2')).toThrow(
/Refusing .* \(.* symbolic link\)/,
);
});
});
describe('isAllowListed', {}, () => {
it.skipIf(process.platform === 'win32')('matches the canonical kubelet projected-token path', () => {
expect(helpers.isAllowListed('/var/run/secrets/eks.amazonaws.com/serviceaccount/token')).toBe(true);
expect(helpers.isAllowListed('/var/run/secrets/kubernetes.io/serviceaccount/token')).toBe(true);
});
it.skipIf(process.platform === 'win32')('rejects nested or unrelated paths', () => {
expect(helpers.isAllowListed('/var/run/secrets/serviceaccount/token')).toBe(false);
expect(helpers.isAllowListed('/var/run/secrets/a/b/serviceaccount/token')).toBe(false);
expect(helpers.isAllowListed('/var/run/secrets/eks.amazonaws.com/serviceaccount/token2')).toBe(false);
expect(helpers.isAllowListed('/etc/var/run/secrets/foo/serviceaccount/token')).toBe(false);
});
it.skipIf(process.platform === 'win32')('normalizes path traversal attempts', () => {
expect(helpers.isAllowListed('/var/run/secrets/foo/serviceaccount/../../../../etc/passwd')).toBe(false);
});
});
describe('writeFileUtf8', {}, () => {
it('writes content with the specified mode', {}, () => {
fs.mkdirSync('/dir', { recursive: true });
helpers.writeFileUtf8('/dir/file', 'payload', 0o600);
expect(fs.readFileSync('/dir/file', 'utf-8')).toBe('payload');
expect(fs.statSync('/dir/file').mode & 0o777).toBe(0o600);
});
it('refuses to follow a symlink at the target and leaves the target file untouched', {}, () => {
fs.mkdirSync('/dir', { recursive: true });
fs.writeFileSync('/dir/target', 'original');
fs.symlinkSync('/dir/target', '/dir/link');
expect(() => helpers.writeFileUtf8('/dir/link', 'attacker', 0o600)).toThrow(/Refusing .* \(.* symbolic link\)/);
expect(fs.readFileSync('/dir/target', 'utf-8')).toBe('original');
});
it.skipIf(process.platform === 'win32')('tightens mode on existing files', () => {
fs.mkdirSync('/dir', { recursive: true });
fs.writeFileSync('/dir/file', 'old', { mode: 0o644 });
helpers.writeFileUtf8('/dir/file', 'new', 0o600);
expect(fs.statSync('/dir/file').mode & 0o777).toBe(0o600);
});
});
describe('mkdir', {}, () => {
it('is idempotent on a regular directory', {}, () => {
helpers.mkdir('/some/nested/dir', 0o700);
helpers.mkdir('/some/nested/dir', 0o700);
expect(fs.statSync('/some/nested/dir').isDirectory()).toBe(true);
});
it('refuses when the target directory is a symlink', {}, () => {
fs.mkdirSync('/real', { recursive: true });
fs.mkdirSync('/home', { recursive: true });
fs.symlinkSync('/real', '/home/.aws');
expect(() => helpers.mkdir('/home/.aws', 0o700)).toThrow(/Refusing .* \(.* symbolic link\)/);
});
});
});
}); });

View file

@ -3,7 +3,6 @@ import {
AssumeRoleCommand, AssumeRoleCommand,
AssumeRoleWithWebIdentityCommand, AssumeRoleWithWebIdentityCommand,
GetCallerIdentityCommand, GetCallerIdentityCommand,
PackedPolicyTooLargeException,
STSClient, STSClient,
} from '@aws-sdk/client-sts'; } from '@aws-sdk/client-sts';
import { mockClient } from 'aws-sdk-client-mock'; import { mockClient } from 'aws-sdk-client-mock';
@ -203,18 +202,6 @@ describe('Configure AWS Credentials', {}, () => {
expect(core.setOutput).toHaveBeenCalledTimes(2); expect(core.setOutput).toHaveBeenCalledTimes(2);
expect(core.setFailed).not.toHaveBeenCalled(); expect(core.setFailed).not.toHaveBeenCalled();
}); });
it('does not send Tags or TransitiveTagKeys to AssumeRoleWithWebIdentity', async () => {
// AssumeRoleWithWebIdentity reads session tags from JWT claims, not the request.
// Both fields must be stripped before the STS call.
vi.mocked(core.getMultilineInput).mockImplementation((name: string) => {
if (name === 'transitive-tag-keys') return ['Repository'];
return [];
});
await run();
const callInput = mockedSTSClient.commandCalls(AssumeRoleWithWebIdentityCommand)[0].args[0].input;
expect(callInput.Tags).toBeUndefined();
expect(callInput.TransitiveTagKeys).toBeUndefined();
});
}); });
describe('Assume existing role', {}, () => { describe('Assume existing role', {}, () => {
@ -295,9 +282,9 @@ describe('Configure AWS Credentials', {}, () => {
await run(); await run();
const tags = mockedSTSClient.commandCalls(AssumeRoleCommand)[0].args[0].input.Tags ?? []; const tags = mockedSTSClient.commandCalls(AssumeRoleCommand)[0].args[0].input.Tags ?? [];
// 7 protected (GitHub + Repository, Workflow, Action, Actor, Commit, Branch) // 7 protected (GitHub + Repository, Workflow, Action, Actor, Commit, Branch)
// + 6 droppable (EventName, BaseRef, HeadRef, RunId, Job, TriggeringActor). // + 8 overrideable (EventName, BaseRef, HeadRef, RefName, RunId, RefType, Job, TriggeringActor).
// No custom-tags, all env vars set in mocks.envs → all 13 should be present, nothing else. // No custom-tags, all env vars set in mocks.envs → all 15 should be present, nothing else.
expect(tags).toHaveLength(13); expect(tags).toHaveLength(15);
const tagsByKey = Object.fromEntries(tags.map((t) => [t.Key, t.Value])); const tagsByKey = Object.fromEntries(tags.map((t) => [t.Key, t.Value]));
expect(tagsByKey).toEqual({ expect(tagsByKey).toEqual({
GitHub: 'Actions', GitHub: 'Actions',
@ -310,12 +297,14 @@ describe('Configure AWS Credentials', {}, () => {
EventName: 'pull_request', EventName: 'pull_request',
BaseRef: 'main', BaseRef: 'main',
HeadRef: 'feature-branch', HeadRef: 'feature-branch',
RefName: 'feature-branch',
RunId: '16412345678', RunId: '16412345678',
RefType: 'branch',
Job: 'build', Job: 'build',
TriggeringActor: 'MY-USERNAME_bot_', TriggeringActor: 'MY-USERNAME_bot_',
}); });
}); });
it('omits droppable tags whose env vars are unset', {}, async () => { it('omits overrideable tags whose env vars are unset', {}, async () => {
vi.mocked(core.getInput).mockImplementation(mocks.getInput(mocks.IAM_ASSUMEROLE_INPUTS)); vi.mocked(core.getInput).mockImplementation(mocks.getInput(mocks.IAM_ASSUMEROLE_INPUTS));
delete process.env.GITHUB_BASE_REF; delete process.env.GITHUB_BASE_REF;
delete process.env.GITHUB_HEAD_REF; delete process.env.GITHUB_HEAD_REF;
@ -329,27 +318,6 @@ describe('Configure AWS Credentials', {}, () => {
expect(tagKeys).toContain('EventName'); expect(tagKeys).toContain('EventName');
expect(tagKeys).toContain('RunId'); expect(tagKeys).toContain('RunId');
}); });
it('drops droppable tags and retries on PackedPolicyTooLargeException', {}, async () => {
vi.mocked(core.getInput).mockImplementation(mocks.getInput(mocks.IAM_ASSUMEROLE_INPUTS));
mockedSTSClient
.on(AssumeRoleCommand)
.rejectsOnce(new PackedPolicyTooLargeException({ message: 'too large', $metadata: {} }))
.resolvesOnce(mocks.outputs.STS_CREDENTIALS);
await run();
expect(core.info).toHaveBeenCalledWith('Session tag size is too large; dropping droppable tags and retrying.');
const retryInput = mockedSTSClient.commandCalls(AssumeRoleCommand)[1].args[0].input;
const retryTagKeys = (retryInput.Tags ?? []).map((t) => t.Key);
expect(retryTagKeys).not.toContain('EventName');
expect(retryTagKeys).not.toContain('BaseRef');
expect(retryTagKeys).not.toContain('HeadRef');
expect(retryTagKeys).not.toContain('RunId');
expect(retryTagKeys).not.toContain('Job');
expect(retryTagKeys).not.toContain('TriggeringActor');
// Protected tags remain
expect(retryTagKeys).toContain('GitHub');
expect(retryTagKeys).toContain('Repository');
expect(core.setFailed).not.toHaveBeenCalled();
});
it('sanitizes invalid characters in env-derived tag values', {}, async () => { it('sanitizes invalid characters in env-derived tag values', {}, async () => {
vi.mocked(core.getInput).mockImplementation(mocks.getInput(mocks.IAM_ASSUMEROLE_INPUTS)); vi.mocked(core.getInput).mockImplementation(mocks.getInput(mocks.IAM_ASSUMEROLE_INPUTS));
process.env.GITHUB_HEAD_REF = 'feature/has spaces&bad?chars'; process.env.GITHUB_HEAD_REF = 'feature/has spaces&bad?chars';
@ -402,6 +370,8 @@ describe('Configure AWS Credentials', {}, () => {
{ Key: 'EventName', Value: 'pull_request' }, { Key: 'EventName', Value: 'pull_request' },
{ Key: 'RunId', Value: '16412345678' }, { Key: 'RunId', Value: '16412345678' },
{ Key: 'Job', Value: 'build' }, { Key: 'Job', Value: 'build' },
{ Key: 'RefName', Value: 'feature-branch' },
{ Key: 'RefType', Value: 'branch' },
{ Key: 'TriggeringActor', Value: 'MY-USERNAME_bot_' }, { Key: 'TriggeringActor', Value: 'MY-USERNAME_bot_' },
{ Key: 'Environment', Value: 'Production' }, { Key: 'Environment', Value: 'Production' },
{ Key: 'Team', Value: 'DevOps' }, { Key: 'Team', Value: 'DevOps' },
@ -450,7 +420,7 @@ describe('Configure AWS Credentials', {}, () => {
await run(); await run();
expect(core.warning).toHaveBeenCalledWith(expect.stringContaining("'custom-tags' is set but will be ignored")); expect(core.warning).toHaveBeenCalledWith(expect.stringContaining("'custom-tags' is set but will be ignored"));
}); });
it('rejects custom tags that conflict with droppable tag keys', {}, async () => { it('lets custom tags override overrideable default tag keys', {}, async () => {
vi.mocked(core.getInput).mockImplementation( vi.mocked(core.getInput).mockImplementation(
mocks.getInput({ mocks.getInput({
...mocks.IAM_ASSUMEROLE_INPUTS, ...mocks.IAM_ASSUMEROLE_INPUTS,
@ -458,10 +428,13 @@ describe('Configure AWS Credentials', {}, () => {
}), }),
); );
await run(); await run();
expect(core.setFailed).toHaveBeenCalledWith( const tags = mockedSTSClient.commandCalls(AssumeRoleCommand)[0].args[0].input.Tags ?? [];
"custom-tags: key 'EventName' conflicts with a protected session tag set by this action and cannot be overridden", const eventNameTags = tags.filter((t) => t.Key === 'EventName');
); const baseRefTags = tags.filter((t) => t.Key === 'BaseRef');
expect(mockedSTSClient.commandCalls(AssumeRoleCommand)).toHaveLength(0); expect(eventNameTags).toHaveLength(1);
expect(eventNameTags[0]?.Value).toBe('workflow_dispatch');
expect(baseRefTags).toHaveLength(1);
expect(baseRefTags[0]?.Value).toBe('release/2026');
}); });
it('rejects custom tags that conflict with the protected Branch tag', {}, async () => { it('rejects custom tags that conflict with the protected Branch tag', {}, async () => {
// Regression guard: Branch was a default before v6.2 and must remain unoverridable. // Regression guard: Branch was a default before v6.2 and must remain unoverridable.
@ -477,10 +450,62 @@ describe('Configure AWS Credentials', {}, () => {
); );
expect(mockedSTSClient.commandCalls(AssumeRoleCommand)).toHaveLength(0); expect(mockedSTSClient.commandCalls(AssumeRoleCommand)).toHaveLength(0);
}); });
it('rejects custom-tags that would exceed the session-tag limit', {}, async () => { it('drops lower-priority overrideable tags when custom-tags would exceed the session-tag limit', {}, async () => {
// 13 existing tags (7 non-droppable + 6 droppable) + 38 custom = 51 > 50. // 7 protected (GitHub + 6 from PROTECTED_TAG_SOURCES) + 40 custom = 47 used → 3 overrideable slots.
// The first 3 overrideable tags by priority are EventName, BaseRef, HeadRef (RefName, RunId, RefType,
// Job, TriggeringActor must be dropped).
const customTagsObj: Record<string, string> = {}; const customTagsObj: Record<string, string> = {};
for (let i = 0; i < 38; i++) { for (let i = 0; i < 40; i++) {
customTagsObj[`Custom${i}`] = `value${i}`;
}
vi.mocked(core.getInput).mockImplementation(
mocks.getInput({
...mocks.IAM_ASSUMEROLE_INPUTS,
'custom-tags': JSON.stringify(customTagsObj),
}),
);
await run();
const tags = mockedSTSClient.commandCalls(AssumeRoleCommand)[0].args[0].input.Tags ?? [];
const tagKeys = tags.map((t) => t.Key);
expect(tags).toHaveLength(50);
expect(tagKeys).toContain('Branch');
expect(tagKeys).toContain('EventName');
expect(tagKeys).toContain('BaseRef');
expect(tagKeys).toContain('HeadRef');
expect(tagKeys).not.toContain('RefName');
expect(tagKeys).not.toContain('RunId');
expect(tagKeys).not.toContain('RefType');
expect(tagKeys).not.toContain('Job');
expect(tagKeys).not.toContain('TriggeringActor');
});
it('overridden overrideable tags free a slot for a lower-priority overrideable tag', {}, async () => {
// Same 40-custom-tag scenario as above, but one of the customs overrides BaseRef.
// BaseRef no longer competes for the overrideable budget, so the next-priority overrideable (RefName) gets in.
const customTagsObj: Record<string, string> = { BaseRef: 'release/2026' };
for (let i = 0; i < 39; i++) {
customTagsObj[`Custom${i}`] = `value${i}`;
}
vi.mocked(core.getInput).mockImplementation(
mocks.getInput({
...mocks.IAM_ASSUMEROLE_INPUTS,
'custom-tags': JSON.stringify(customTagsObj),
}),
);
await run();
const tags = mockedSTSClient.commandCalls(AssumeRoleCommand)[0].args[0].input.Tags ?? [];
const tagKeys = tags.map((t) => t.Key);
expect(tags).toHaveLength(50);
expect(tagKeys).toContain('Branch');
expect(tagKeys).toContain('EventName');
expect(tagKeys).toContain('BaseRef');
expect(tagKeys).toContain('HeadRef');
expect(tagKeys).toContain('RefName');
expect(tagKeys).not.toContain('RunId');
});
it('rejects custom-tags that would exceed the session-tag limit on their own', {}, async () => {
// 7 protected + 44 custom = 51, which is over 50 even with zero overrideable tags.
const customTagsObj: Record<string, string> = {};
for (let i = 0; i < 44; i++) {
customTagsObj[`Custom${i}`] = `value${i}`; customTagsObj[`Custom${i}`] = `value${i}`;
} }
vi.mocked(core.getInput).mockImplementation( vi.mocked(core.getInput).mockImplementation(
@ -493,10 +518,12 @@ describe('Configure AWS Credentials', {}, () => {
expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining('would exceed the AWS limit of 50')); expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining('would exceed the AWS limit of 50'));
expect(mockedSTSClient.commandCalls(AssumeRoleCommand)).toHaveLength(0); expect(mockedSTSClient.commandCalls(AssumeRoleCommand)).toHaveLength(0);
}); });
it('allows custom-tags up to the session-tag limit', {}, async () => { it('drops transitive-tag-keys entries that refer to evicted overrideable tags', {}, async () => {
// 13 existing tags + 37 custom = 50, exactly at the limit. // Force eviction of all overrideable tags below EventName/BaseRef/HeadRef. The user transitive-tags
// RunId (which gets evicted) and Repository (which is protected and stays). The TransitiveTagKeys
// payload must include only the keys that actually appear in Tags.
const customTagsObj: Record<string, string> = {}; const customTagsObj: Record<string, string> = {};
for (let i = 0; i < 37; i++) { for (let i = 0; i < 40; i++) {
customTagsObj[`Custom${i}`] = `value${i}`; customTagsObj[`Custom${i}`] = `value${i}`;
} }
vi.mocked(core.getInput).mockImplementation( vi.mocked(core.getInput).mockImplementation(
@ -505,10 +532,15 @@ describe('Configure AWS Credentials', {}, () => {
'custom-tags': JSON.stringify(customTagsObj), 'custom-tags': JSON.stringify(customTagsObj),
}), }),
); );
vi.mocked(core.getMultilineInput).mockImplementation((name: string) => {
if (name === 'transitive-tag-keys') return ['Repository', 'RunId'];
return [];
});
await run(); await run();
expect(core.setFailed).not.toHaveBeenCalled(); const callInput = mockedSTSClient.commandCalls(AssumeRoleCommand)[0].args[0].input;
const tags = mockedSTSClient.commandCalls(AssumeRoleCommand)[0].args[0].input.Tags ?? []; const tagKeys = (callInput.Tags ?? []).map((t) => t.Key);
expect(tags).toHaveLength(50); expect(tagKeys).not.toContain('RunId');
expect(callInput.TransitiveTagKeys).toEqual(['Repository']);
}); });
}); });

View file

@ -105,6 +105,8 @@ const envs = {
GITHUB_EVENT_NAME: 'pull_request', GITHUB_EVENT_NAME: 'pull_request',
GITHUB_RUN_ID: '16412345678', GITHUB_RUN_ID: '16412345678',
GITHUB_JOB: 'build', GITHUB_JOB: 'build',
GITHUB_REF_NAME: 'feature-branch',
GITHUB_REF_TYPE: 'branch',
GITHUB_BASE_REF: 'main', GITHUB_BASE_REF: 'main',
GITHUB_HEAD_REF: 'feature-branch', GITHUB_HEAD_REF: 'feature-branch',
GITHUB_TRIGGERING_ACTOR: 'MY-USERNAME[bot]', GITHUB_TRIGGERING_ACTOR: 'MY-USERNAME[bot]',

View file

@ -11,13 +11,12 @@ import {
writeProfileFiles, writeProfileFiles,
} from '../src/profileManager'; } from '../src/profileManager';
vi.mock('node:fs');
vi.mock('@actions/core'); vi.mock('@actions/core');
vi.mock('node:fs');
describe('Profile Manager', {}, () => { describe('Profile Manager', {}, () => {
beforeEach(() => { beforeEach(() => {
vi.restoreAllMocks(); vi.resetAllMocks();
vi.clearAllMocks();
vol.reset(); vol.reset();
}); });
@ -736,69 +735,4 @@ describe('Profile Manager', {}, () => {
); );
}); });
}); });
describe('symlink hardening', {}, () => {
const credsPath = '/home/user/.aws/credentials';
const configPath = '/home/user/.aws/config';
beforeEach(() => {
process.env.AWS_SHARED_CREDENTIALS_FILE = credsPath;
process.env.AWS_CONFIG_FILE = configPath;
});
it('mergeProfileSection refuses when the credentials path is a symlink and leaves the target unchanged', {}, () => {
fs.mkdirSync('/home/user/.aws', { recursive: true });
fs.mkdirSync('/etc', { recursive: true });
fs.writeFileSync('/etc/passwd', 'root:x:0:0::/root:/bin/sh');
fs.symlinkSync('/etc/passwd', credsPath);
expect(() => mergeProfileSection(credsPath, 'dev', { aws_access_key_id: 'AKIA' }, true)).toThrow(
/Refusing .* \(.* symbolic link\)/,
);
expect(fs.readFileSync('/etc/passwd', 'utf-8')).toBe('root:x:0:0::/root:/bin/sh');
});
it('mergeProfileSection refuses when the config path is a symlink', {}, () => {
fs.mkdirSync('/home/user/.aws', { recursive: true });
fs.mkdirSync('/etc', { recursive: true });
fs.writeFileSync('/etc/sensitive', 'do not overwrite');
fs.symlinkSync('/etc/sensitive', configPath);
expect(() => mergeProfileSection(configPath, 'profile dev', { region: 'us-east-1' }, true)).toThrow(
/Refusing .* \(.* symbolic link\)/,
);
expect(fs.readFileSync('/etc/sensitive', 'utf-8')).toBe('do not overwrite');
});
it('ensureAwsDirectoryExists refuses when ~/.aws is a symlink', {}, () => {
fs.mkdirSync('/real-target', { recursive: true });
fs.mkdirSync('/home/user', { recursive: true });
fs.symlinkSync('/real-target', '/home/user/.aws');
expect(() => ensureAwsDirectoryExists(credsPath)).toThrow(/Refusing .* \(.* symbolic link\)/);
});
it('writeProfileFiles refuses to overwrite a pre-existing symlink at the credentials path', {}, () => {
fs.mkdirSync('/home/user/.aws', { recursive: true });
fs.mkdirSync('/etc', { recursive: true });
fs.writeFileSync('/etc/passwd', 'root:x:0:0::/root:/bin/sh');
fs.symlinkSync('/etc/passwd', credsPath);
expect(() =>
writeProfileFiles('dev', { AccessKeyId: 'AKIA', SecretAccessKey: 'secret' }, 'us-east-1', true),
).toThrow(/Refusing .* \(.* symbolic link\)/);
expect(fs.lstatSync(credsPath).isSymbolicLink()).toBe(true);
expect(fs.readFileSync('/etc/passwd', 'utf-8')).toBe('root:x:0:0::/root:/bin/sh');
});
it('happy path still writes both files with mode 0o600 when no symlinks are present', {}, () => {
writeProfileFiles('dev', { AccessKeyId: 'AKIA', SecretAccessKey: 'secret' }, 'us-east-1', false);
expect(fs.statSync(credsPath).mode & 0o777).toBe(0o600);
expect(fs.statSync(configPath).mode & 0o777).toBe(0o600);
expect(fs.lstatSync(credsPath).isSymbolicLink()).toBe(false);
expect(fs.lstatSync(configPath).isSymbolicLink()).toBe(false);
});
});
}); });