├── .github └── workflows │ ├── release.yml │ ├── static-analysis.yml │ └── test.yml ├── .gitignore ├── .goreleaser.yaml ├── Formula └── managed-kubernetes-auditing-toolkit.rb ├── LICENSE ├── LICENSE-3rdparty.csv ├── Makefile ├── NOTICE ├── README.md ├── cmd └── managed-kubernetes-auditing-toolkit │ ├── eks │ ├── find_secrets.go │ ├── imds.go │ ├── main.go │ └── role_relationships.go │ └── main.go ├── examples ├── demo-cluster │ ├── README.md │ └── terraform │ │ ├── .gitignore │ │ ├── .terraform.lock.hcl │ │ ├── main.tf │ │ ├── objects.yaml │ │ ├── pods.tf │ │ ├── roles.tf │ │ ├── secrets.tf │ │ ├── serviceaccounts.tf │ │ ├── variables.tf │ │ └── versions.tf └── irsa.png ├── go.mod ├── go.sum ├── internal ├── aws │ └── iam_evaluation │ │ ├── authorization.go │ │ ├── condition.go │ │ ├── condition_test.go │ │ ├── policy.go │ │ ├── policy_parser.go │ │ ├── policy_parser_test.go │ │ ├── policy_test.go │ │ ├── statement.go │ │ ├── statement_test.go │ │ └── test_policies │ │ ├── allow_assume_by_ec2.json │ │ ├── allow_oidc_with_condition.json │ │ ├── eks_irsa.json │ │ └── eks_irsa_stringlike.json └── utils │ ├── aws.go │ ├── case_insensitive_map.go │ ├── file.go │ └── kubernetes.go ├── permissions.md └── pkg └── managed-kubernetes-auditing-toolkit └── eks ├── imds └── imds_tester.go ├── role_relationships └── roles_resolver.go ├── secrets ├── aws_secrets.go ├── aws_secrets_detector.go ├── aws_secrets_test.go ├── configmap.go ├── configmap_test.go ├── pod.go ├── pod_test.go ├── secret.go └── secret_test.go ├── types.go └── utils.go /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | permissions: 9 | contents: write 10 | pull-requests: write 11 | 12 | jobs: 13 | goreleaser: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v2.5.0 18 | with: 19 | fetch-depth: 0 20 | - name: Set up Go 21 | uses: actions/setup-go@v3.3.1 22 | with: 23 | go-version: 1.19 24 | - name: Run GoReleaser 25 | timeout-minutes: 60 26 | uses: goreleaser/goreleaser-action@v6.1.0 27 | with: 28 | distribution: goreleaser 29 | version: latest 30 | args: release --clean --config .goreleaser.yaml 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | -------------------------------------------------------------------------------- /.github/workflows/static-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "go static analysis" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | static-analysis: 13 | name: "Run Go static analysis" 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 1 19 | - name: Set up Go 20 | uses: actions/setup-go@v2 21 | with: 22 | go-version: 1.23 23 | - uses: dominikh/staticcheck-action@v1.3.1 24 | with: 25 | version: "2024.1" 26 | install-go: false 27 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | unit-test: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v2 20 | 21 | - name: Set up Go 22 | uses: actions/setup-go@v2 23 | with: 24 | go-version: 1.18 25 | 26 | - name: Run unit tests 27 | run: make test 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | ./mkat 3 | bin 4 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | before: 3 | hooks: 4 | - go mod tidy 5 | builds: 6 | - env: 7 | - CGO_ENABLED=0 8 | goos: 9 | - linux 10 | - windows 11 | - darwin 12 | ldflags: 13 | - -X main.BuildVersion={{.Version}} 14 | 15 | dir: cmd/managed-kubernetes-auditing-toolkit 16 | binary: mkat 17 | brews: 18 | - name: managed-kubernetes-auditing-toolkit 19 | repository: 20 | owner: datadog 21 | name: managed-kubernetes-auditing-toolkit 22 | branch: "homebrew-update-{{ .Version }}" 23 | pull_request: 24 | enabled: true 25 | base: 26 | owner: datadog 27 | name: managed-kubernetes-auditing-toolkit 28 | branch: main 29 | directory: Formula 30 | url_template: "https://github.com/DataDog/managed-kubernetes-auditing-toolkit/releases/download/{{ .Tag }}/{{ .ArtifactName }}" 31 | license: Apache-2.0 32 | homepage: "https://github.com/DataDog/managed-kubernetes-auditing-toolkit" 33 | archives: 34 | - name_template: >- 35 | {{ .ProjectName }}_ 36 | {{- title .Os }}_ 37 | {{- if eq .Arch "amd64" }}x86_64 38 | {{- else if eq .Arch "386" }}i386 39 | {{- else }}{{ .Arch }}{{ end }} 40 | checksum: 41 | name_template: 'checksums.txt' 42 | snapshot: 43 | name_template: "{{ incpatch .Version }}-next" 44 | changelog: 45 | sort: asc 46 | filters: 47 | exclude: 48 | - '^docs:' 49 | - '^test:' 50 | -------------------------------------------------------------------------------- /Formula/managed-kubernetes-auditing-toolkit.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | # frozen_string_literal: true 3 | 4 | # This file was generated by GoReleaser. DO NOT EDIT. 5 | class ManagedKubernetesAuditingToolkit < Formula 6 | desc "" 7 | homepage "https://github.com/DataDog/managed-kubernetes-auditing-toolkit" 8 | version "0.3.1" 9 | license "Apache-2.0" 10 | 11 | on_macos do 12 | if Hardware::CPU.intel? 13 | url "https://github.com/DataDog/managed-kubernetes-auditing-toolkit/releases/download/v0.3.1/managed-kubernetes-auditing-toolkit_Darwin_x86_64.tar.gz" 14 | sha256 "7e419f2315c7bfc48afda35492ede36b9384f908b4d8e97d15853747e623b305" 15 | 16 | def install 17 | bin.install "mkat" 18 | end 19 | end 20 | if Hardware::CPU.arm? 21 | url "https://github.com/DataDog/managed-kubernetes-auditing-toolkit/releases/download/v0.3.1/managed-kubernetes-auditing-toolkit_Darwin_arm64.tar.gz" 22 | sha256 "f1d17ba7bedc3e17e343d9b5c5dc2a2ccc23b6154e956eb1d0fd9fdfb12e60eb" 23 | 24 | def install 25 | bin.install "mkat" 26 | end 27 | end 28 | end 29 | 30 | on_linux do 31 | if Hardware::CPU.intel? and Hardware::CPU.is_64_bit? 32 | url "https://github.com/DataDog/managed-kubernetes-auditing-toolkit/releases/download/v0.3.1/managed-kubernetes-auditing-toolkit_Linux_x86_64.tar.gz" 33 | sha256 "37f361925d53d5aa1d1f6b27a40c287b05b90f3b066f09a1f0ce0c985a3ea16f" 34 | def install 35 | bin.install "mkat" 36 | end 37 | end 38 | if Hardware::CPU.arm? and Hardware::CPU.is_64_bit? 39 | url "https://github.com/DataDog/managed-kubernetes-auditing-toolkit/releases/download/v0.3.1/managed-kubernetes-auditing-toolkit_Linux_arm64.tar.gz" 40 | sha256 "93175d144e260e3f75e709c8a1ee33a64e9f3cda549bcd9c40a3618a8cd106bd" 41 | def install 42 | bin.install "mkat" 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-3rdparty.csv: -------------------------------------------------------------------------------- 1 | github.com/awalterschulze/gographviz,https://github.com/awalterschulze/gographviz/blob/v2.0.3/LICENSE,BSD-3-Clause 2 | github.com/aws/aws-sdk-go-v2,https://github.com/aws/aws-sdk-go-v2/blob/v1.17.6/LICENSE.txt,Apache-2.0 3 | github.com/aws/aws-sdk-go-v2/config,https://github.com/aws/aws-sdk-go-v2/blob/config/v1.18.16/config/LICENSE.txt,Apache-2.0 4 | github.com/aws/aws-sdk-go-v2/credentials,https://github.com/aws/aws-sdk-go-v2/blob/credentials/v1.13.16/credentials/LICENSE.txt,Apache-2.0 5 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds,https://github.com/aws/aws-sdk-go-v2/blob/feature/ec2/imds/v1.12.24/feature/ec2/imds/LICENSE.txt,Apache-2.0 6 | github.com/aws/aws-sdk-go-v2/internal/configsources,https://github.com/aws/aws-sdk-go-v2/blob/internal/configsources/v1.1.30/internal/configsources/LICENSE.txt,Apache-2.0 7 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2,https://github.com/aws/aws-sdk-go-v2/blob/internal/endpoints/v2.4.24/internal/endpoints/v2/LICENSE.txt,Apache-2.0 8 | github.com/aws/aws-sdk-go-v2/internal/ini,https://github.com/aws/aws-sdk-go-v2/blob/internal/ini/v1.3.31/internal/ini/LICENSE.txt,Apache-2.0 9 | github.com/aws/aws-sdk-go-v2/internal/sync/singleflight,https://github.com/aws/aws-sdk-go-v2/blob/v1.17.6/internal/sync/singleflight/LICENSE,BSD-3-Clause 10 | github.com/aws/aws-sdk-go-v2/service/eks,https://github.com/aws/aws-sdk-go-v2/blob/service/eks/v1.27.6/service/eks/LICENSE.txt,Apache-2.0 11 | github.com/aws/aws-sdk-go-v2/service/iam,https://github.com/aws/aws-sdk-go-v2/blob/service/iam/v1.19.5/service/iam/LICENSE.txt,Apache-2.0 12 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url,https://github.com/aws/aws-sdk-go-v2/blob/service/internal/presigned-url/v1.9.24/service/internal/presigned-url/LICENSE.txt,Apache-2.0 13 | github.com/aws/aws-sdk-go-v2/service/sso,https://github.com/aws/aws-sdk-go-v2/blob/service/sso/v1.12.5/service/sso/LICENSE.txt,Apache-2.0 14 | github.com/aws/aws-sdk-go-v2/service/ssooidc,https://github.com/aws/aws-sdk-go-v2/blob/service/ssooidc/v1.14.5/service/ssooidc/LICENSE.txt,Apache-2.0 15 | github.com/aws/aws-sdk-go-v2/service/sts,https://github.com/aws/aws-sdk-go-v2/blob/service/sts/v1.18.6/service/sts/LICENSE.txt,Apache-2.0 16 | github.com/aws/smithy-go,https://github.com/aws/smithy-go/blob/v1.13.5/LICENSE,Apache-2.0 17 | github.com/aws/smithy-go/internal/sync/singleflight,https://github.com/aws/smithy-go/blob/v1.13.5/internal/sync/singleflight/LICENSE,BSD-3-Clause 18 | github.com/common-nighthawk/go-figure,https://github.com/common-nighthawk/go-figure/blob/734e95fb86be/LICENSE,MIT 19 | github.com/datadog/managed-kubernetes-auditing-toolkit,https://github.com/datadog/managed-kubernetes-auditing-toolkit/blob/HEAD/LICENSE,Apache-2.0 20 | github.com/davecgh/go-spew/spew,https://github.com/davecgh/go-spew/blob/v1.1.1/LICENSE,ISC 21 | github.com/emicklei/go-restful/v3,https://github.com/emicklei/go-restful/blob/v3.9.0/LICENSE,MIT 22 | github.com/fatih/color,https://github.com/fatih/color/blob/v1.15.0/LICENSE.md,MIT 23 | github.com/go-logr/logr,https://github.com/go-logr/logr/blob/v1.2.3/LICENSE,Apache-2.0 24 | github.com/go-openapi/jsonpointer,https://github.com/go-openapi/jsonpointer/blob/v0.19.5/LICENSE,Apache-2.0 25 | github.com/go-openapi/jsonreference,https://github.com/go-openapi/jsonreference/blob/v0.20.0/LICENSE,Apache-2.0 26 | github.com/go-openapi/swag,https://github.com/go-openapi/swag/blob/v0.19.14/LICENSE,Apache-2.0 27 | github.com/gogo/protobuf,https://github.com/gogo/protobuf/blob/v1.3.2/LICENSE,BSD-3-Clause 28 | github.com/golang/protobuf,https://github.com/golang/protobuf/blob/v1.5.2/LICENSE,BSD-3-Clause 29 | github.com/google/gnostic,https://github.com/google/gnostic/blob/v0.5.7-v3refs/LICENSE,Apache-2.0 30 | github.com/google/go-cmp/cmp,https://github.com/google/go-cmp/blob/v0.5.9/LICENSE,BSD-3-Clause 31 | github.com/google/gofuzz,https://github.com/google/gofuzz/blob/v1.1.0/LICENSE,Apache-2.0 32 | github.com/imdario/mergo,https://github.com/imdario/mergo/blob/v0.3.6/LICENSE,BSD-3-Clause 33 | github.com/jedib0t/go-pretty/v6,https://github.com/jedib0t/go-pretty/blob/v6.4.6/LICENSE,MIT 34 | github.com/jmespath/go-jmespath,https://github.com/jmespath/go-jmespath/blob/v0.4.0/LICENSE,Apache-2.0 35 | github.com/josharian/intern,https://github.com/josharian/intern/blob/v1.0.0/license.md,MIT 36 | github.com/json-iterator/go,https://github.com/json-iterator/go/blob/v1.1.12/LICENSE,MIT 37 | github.com/mailru/easyjson,https://github.com/mailru/easyjson/blob/v0.7.6/LICENSE,MIT 38 | github.com/mattn/go-colorable,https://github.com/mattn/go-colorable/blob/v0.1.13/LICENSE,MIT 39 | github.com/mattn/go-isatty,https://github.com/mattn/go-isatty/blob/v0.0.17/LICENSE,MIT 40 | github.com/mattn/go-runewidth,https://github.com/mattn/go-runewidth/blob/v0.0.13/LICENSE,MIT 41 | github.com/modern-go/concurrent,https://github.com/modern-go/concurrent/blob/bacd9c7ef1dd/LICENSE,Apache-2.0 42 | github.com/modern-go/reflect2,https://github.com/modern-go/reflect2/blob/v1.0.2/LICENSE,Apache-2.0 43 | github.com/munnerz/goautoneg,https://github.com/munnerz/goautoneg/blob/a7dc8b61c822/LICENSE,BSD-3-Clause 44 | github.com/rivo/uniseg,https://github.com/rivo/uniseg/blob/v0.2.0/LICENSE.txt,MIT 45 | github.com/spf13/cobra,https://github.com/spf13/cobra/blob/v1.6.1/LICENSE.txt,Apache-2.0 46 | github.com/spf13/pflag,https://github.com/spf13/pflag/blob/v1.0.5/LICENSE,BSD-3-Clause 47 | golang.org/x/exp,https://cs.opensource.google/go/x/exp/+/10a50721:LICENSE,BSD-3-Clause 48 | golang.org/x/net,https://cs.opensource.google/go/x/net/+/v0.7.0:LICENSE,BSD-3-Clause 49 | golang.org/x/oauth2,https://cs.opensource.google/go/x/oauth2/+/fd043fe5:LICENSE,BSD-3-Clause 50 | golang.org/x/sys/unix,https://cs.opensource.google/go/x/sys/+/v0.6.0:LICENSE,BSD-3-Clause 51 | golang.org/x/term,https://cs.opensource.google/go/x/term/+/v0.5.0:LICENSE,BSD-3-Clause 52 | golang.org/x/text,https://cs.opensource.google/go/x/text/+/v0.7.0:LICENSE,BSD-3-Clause 53 | golang.org/x/time/rate,https://cs.opensource.google/go/x/time/+/90d013bb:LICENSE,BSD-3-Clause 54 | google.golang.org/protobuf,https://github.com/protocolbuffers/protobuf-go/blob/v1.28.1/LICENSE,BSD-3-Clause 55 | gopkg.in/inf.v0,https://github.com/go-inf/inf/blob/v0.9.1/LICENSE,BSD-3-Clause 56 | gopkg.in/yaml.v2,https://github.com/go-yaml/yaml/blob/v2.4.0/LICENSE,Apache-2.0 57 | gopkg.in/yaml.v3,https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE,MIT 58 | k8s.io/api,https://github.com/kubernetes/api/blob/v0.26.2/LICENSE,Apache-2.0 59 | k8s.io/apimachinery/pkg,https://github.com/kubernetes/apimachinery/blob/v0.26.2/LICENSE,Apache-2.0 60 | k8s.io/apimachinery/third_party/forked/golang/reflect,https://github.com/kubernetes/apimachinery/blob/v0.26.2/third_party/forked/golang/LICENSE,BSD-3-Clause 61 | k8s.io/client-go,https://github.com/kubernetes/client-go/blob/v0.26.2/LICENSE,Apache-2.0 62 | k8s.io/klog/v2,https://github.com/kubernetes/klog/blob/v2.80.1/LICENSE,Apache-2.0 63 | k8s.io/kube-openapi/pkg,https://github.com/kubernetes/kube-openapi/blob/172d655c2280/LICENSE,Apache-2.0 64 | k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json,https://github.com/kubernetes/kube-openapi/blob/172d655c2280/pkg/internal/third_party/go-json-experiment/json/LICENSE,BSD-3-Clause 65 | k8s.io/kube-openapi/pkg/validation/spec,https://github.com/kubernetes/kube-openapi/blob/172d655c2280/pkg/validation/spec/LICENSE,Apache-2.0 66 | k8s.io/utils,https://github.com/kubernetes/utils/blob/1a15be271d1d/LICENSE,Apache-2.0 67 | k8s.io/utils/internal/third_party/forked/golang/net,https://github.com/kubernetes/utils/blob/1a15be271d1d/internal/third_party/forked/golang/LICENSE,BSD-3-Clause 68 | sigs.k8s.io/json,https://github.com/kubernetes-sigs/json/blob/f223a00ba0e2/LICENSE,Apache-2.0 69 | sigs.k8s.io/structured-merge-diff/v4,https://github.com/kubernetes-sigs/structured-merge-diff/blob/v4.2.3/LICENSE,Apache-2.0 70 | sigs.k8s.io/yaml,https://github.com/kubernetes-sigs/yaml/blob/v1.3.0/LICENSE,MIT 71 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | MAKEFILE_PATH := $(abspath $(lastword $(MAKEFILE_LIST))) 2 | ROOT_DIR := $(dir $(MAKEFILE_PATH)) 3 | BUILD_VERSION=dev-snapshot 4 | 5 | all: 6 | mkdir -p bin 7 | go build -ldflags="-X main.BuildVersion=$(BUILD_VERSION)" -o bin/mkat ./cmd/managed-kubernetes-auditing-toolkit/main.go 8 | 9 | test: 10 | go test ./... -v 11 | 12 | thirdparty-licenses: 13 | go get github.com/google/go-licenses 14 | go install github.com/google/go-licenses 15 | go-licenses csv github.com/datadog/managed-kubernetes-auditing-toolkit/cmd/managed-kubernetes-auditing-toolkit | sort > $(ROOT_DIR)/LICENSE-3rdparty.csv 16 | 17 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Managed Kubernetes Auditing Toolkit 2 | Copyright 2023-Present Datadog, Inc. 3 | 4 | This product includes software developed at Datadog ( 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Managed Kubernetes Auditing Toolkit (MKAT) 2 | 3 | [![Tests](https://github.com/DataDog/managed-kubernetes-auditing-toolkit/actions/workflows/test.yml/badge.svg)](https://github.com/DataDog/managed-kubernetes-auditing-toolkit/actions/workflows/test.yml) [![go static 4 | analysis](https://github.com/DataDog/managed-kubernetes-auditing-toolkit/actions/workflows/static-analysis.yml/badge.svg)](https://github.com/DataDog/managed-kubernetes-auditing-toolkit/actions/workflows/static-analysis.yml) 5 | 6 | 7 | MKAT is an all-in-one auditing toolkit for identifying common security issues within managed Kubernetes environments. It is focused on Amazon EKS at the moment, and will be extended to other managed Kubernetes environments in the future. 8 | 9 | Features: 10 | - 🔎 [Identify trust relationships between K8s service accounts and AWS IAM roles](#identify-trust-relationships-between-k8s-service-accounts-and-aws-iam-roles) - supports both IAM Roles for Service Accounts (IRSA), and [Pod Identity](https://aws.amazon.com/blogs/aws/amazon-eks-pod-identity-simplifies-iam-permissions-for-applications-on-amazon-eks-clusters/), released on November 26 2023. 11 | - 🔑 [Find hardcoded AWS credentials in K8s resources](#find-hardcoded-aws-credentials-in-k8s-resources). 12 | - 💀 [Test if pods can access the AWS Instance Metadata Service (IMDS)](#test-if-pods-can-access-the-aws-instance-metadata-service-imds). 13 | 14 | ## Installation 15 | 16 | ```bash 17 | brew tap datadog/mkat https://github.com/datadog/managed-kubernetes-auditing-toolkit 18 | brew install datadog/mkat/managed-kubernetes-auditing-toolkit 19 | mkat version 20 | ``` 21 | 22 | ... or use a [pre-compiled binary](https://github.com/DataDog/managed-kubernetes-auditing-toolkit/releases). 23 | 24 | Then, make sure you are authenticated against your cluster, and to AWS. MKAT uses your current AWS and kubectl authentication contexts. 25 | 26 | ```bash 27 | aws eks update-kubeconfig --name 28 | ``` 29 | 30 | In particular, you might need to set your `AWS_REGION` and `AWS_PROFILE` environment variables, if using profiles. 31 | 32 | ## Features 33 | 34 | ### Identify trust relationships between K8s service accounts and AWS IAM roles 35 | 36 | MKAT can identify the trust relationships between K8s service accounts and AWS IAM roles, and display them in a table or as a graph. It currently supports: 37 | 38 | - **[IAM Roles for Service Accounts](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html)**, a popular mechanism to allow pods to assume AWS IAM roles by exchanging a Kubernetes service account token for AWS credentials through the AWS STS API (`AssumeRoleWithWebIdentity`). 39 | 40 | - **[EKS Pod Identity](https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html)**, another newer mechanism that works in a similar way, but is easier to set up. 41 | 42 | MKAT works by analyzing both the IAM roles in the AWS account, and the K8s service accounts in the cluster, and then matching them together based on these two mechanisms. 43 | 44 | ```bash 45 | $ mkat eks find-role-relationships 46 | _ __ ___ | | __ __ _ | |_ 47 | | '_ ` _ \ | |/ / / _` | | __| 48 | | | | | | | | < | (_| | | |_ 49 | |_| |_| |_| |_|\_\ \__,_| \__| 50 | 51 | 2023/11/28 21:05:59 Connected to EKS cluster mkat-cluster 52 | 2023/11/28 21:05:59 Retrieving cluster information 53 | 2023/11/28 21:06:00 Listing K8s service accounts in all namespaces 54 | 2023/11/28 21:06:02 Listing roles in the AWS account 55 | 2023/11/28 21:06:03 Found 286 IAM roles in the AWS account 56 | 2023/11/28 21:06:03 Analyzing IAM Roles For Service Accounts (IRSA) configuration 57 | 2023/11/28 21:06:03 Analyzing Pod Identity configuration of your cluster 58 | 2023/11/28 21:06:04 Analyzing namespace microservices which has 1 Pod Identity associations 59 | +------------------+---------------------------+-----------------------------------+-----------------------------+--------------------------------+ 60 | | NAMESPACE | SERVICE ACCOUNT | POD | ASSUMABLE ROLE | MECHANISM | 61 | +------------------+---------------------------+-----------------------------------+-----------------------------+--------------------------------+ 62 | | microservices | inventory-service-sa | inventory-service | inventory-service-role | IAM Roles for Service Accounts | 63 | | | | | s3-backup-role | IAM Roles for Service Accounts | 64 | | | rate-limiter-sa | rate-limiter-1 | rate-limiter-role | IAM Roles for Service Accounts | 65 | | | | | webserver-role | Pod Identity | 66 | | | | rate-limiter-2 | rate-limiter-role | IAM Roles for Service Accounts | 67 | | | | | webserver-role | Pod Identity | 68 | +------------------+---------------------------+-----------------------------------+-----------------------------+--------------------------------+ 69 | | default | vulnerable-application-sa | vulnerable-application | vulnerable-application-role | IAM Roles for Service Accounts | 70 | | | webserver-sa | webserver | webserver-role | IAM Roles for Service Accounts | 71 | +------------------+---------------------------+-----------------------------------+-----------------------------+--------------------------------+ 72 | | external-secrets | external-secrets-sa | external-secrets-66cfb84c9b-kldt9 | ExternalSecretsRole | IAM Roles for Service Accounts | 73 | +------------------+---------------------------+-----------------------------------+-----------------------------+--------------------------------+ 74 | ``` 75 | 76 | It can also generate a `dot` output for graphic visualization: 77 | 78 | ```bash 79 | $ mkat eks find-role-relationships --output-format dot --output-file roles.dot 80 | $ dot -Tpng -O roles.dot 81 | $ open roles.dot.png 82 | ``` 83 | 84 | ![Mapping trust relationships](./examples/irsa.png) 85 | 86 | ### Find hardcoded AWS credentials in K8s resources 87 | 88 | MKAT can identify hardcoded AWS credentials in K8s resources such as Pods, ConfigMaps, and Secrets. 89 | It has a low false positive rate, and only alerts you if it finds both an AWS access key ID and a secret access key in the same Kubernetes resource. 90 | It's also able to work with unstructured data, i.e. if you have a ConfigMap with an embedded JSON or YAML document that contains AWS credentials. 91 | 92 | ```bash 93 | $ mkat eks find-secrets 94 | _ _ 95 | _ __ ___ | | __ __ _ | |_ 96 | | '_ ` _ \ | |/ / / _` | | __| 97 | | | | | | | | < | (_| | | |_ 98 | |_| |_| |_| |_|\_\ \__,_| \__| 99 | 100 | 2023/04/12 00:33:24 Connected to EKS cluster mkat-cluster 101 | 2023/04/12 00:33:24 Searching for AWS secrets in ConfigMaps... 102 | 2023/04/12 00:33:25 Analyzing 10 ConfigMaps... 103 | 2023/04/12 00:33:25 Searching for AWS secrets in Secrets... 104 | 2023/04/12 00:33:25 Analyzing 45 Secrets... 105 | 2023/04/12 00:33:25 Searching for AWS secrets in Pod definitions... 106 | 2023/04/12 00:33:25 Analyzing 8 Pod definitions... 107 | +-----------+--------+-----------------------------------------+------------------------------------------+ 108 | | NAMESPACE | TYPE | NAME | VALUE | 109 | +-----------+--------+-----------------------------------------+------------------------------------------+ 110 | | default | Secret | kafka-proxy-aws (key aws_access_key_id) | AKIAZ3MSJV4WWNKWW5FG | 111 | | default | Secret | kafka-proxy-aws (key aws_secret_key) | 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 | 112 | +-----------+--------+-----------------------------------------+------------------------------------------+ 113 | ``` 114 | 115 | ### Test if pods can access the AWS Instance Metadata Service (IMDS) 116 | 117 | Pods accessing the EKS nodes Instance Metadata Service is a [common and dangerous attack vector](https://blog.christophetd.fr/privilege-escalation-in-aws-elastic-kubernetes-service-eks-by-compromising-the-instance-role-of-worker-nodes/) 118 | that can be used to escalate privileges. MKAT can test if pods can access the IMDS, both through IMDSv1 and IMDSv2. 119 | 120 | It tests this by creating two temporary pods (one for IMDSv1, one for IMDSv2) that try to access the IMDS, and are then deleted. 121 | 122 | ```bash 123 | $ mkat eks test-imds-access 124 | _ _ 125 | _ __ ___ | | __ __ _ | |_ 126 | | '_ ` _ \ | |/ / / _` | | __| 127 | | | | | | | | < | (_| | | |_ 128 | |_| |_| |_| |_|\_\ \__,_| \__| 129 | 130 | 2023/07/11 21:56:19 Connected to EKS cluster mkat-cluster 131 | 2023/07/11 21:56:19 Testing if IMDSv1 and IMDSv2 are accessible from pods by creating a pod that attempts to access it 132 | 2023/07/11 21:56:23 IMDSv2 is accessible: any pod can retrieve credentials for the AWS role eksctl-mkat-cluster-nodegroup-ng-NodeInstanceRole-AXWUFF35602Z 133 | 2023/07/11 21:56:23 IMDSv1 is not accessible to pods in your cluster: able to establish a network connection to the IMDS, but no credentials were returned 134 | ``` 135 | 136 | ## FAQ 137 | 138 | ### How does MKAT compare to other tools? 139 | 140 | | **Tool** | **Description** | 141 | |:---:|:---:| 142 | | [kube-bench](https://github.com/aquasecurity/kube-bench) | kube-bench is a general-purpose auditing tool for Kubernetes cluster, checking for compliance against the CIS benchmarks | 143 | | [kubiscan](https://github.com/cyberark/KubiScan) | kubiscan focuses on identifying dangerous in-cluster RBAC permissions | 144 | | [peirates](https://github.com/inguardians/peirates) | peirates is a generic Kubernetes penetration testing tool. Although it has a `get-aws-token` command that retrieve node credentials from the IMDS, it is not specific to managed K8s environments. | 145 | | [botb](https://github.com/brompwnie/botb) | botb is a generic Kubernetes penetration testing tool. It also has a command to retrieve node credentials from the IMDS, but it is not specific to managed K8s environments. | 146 | | [rbac-police](https://github.com/PaloAltoNetworks/rbac-police) | rbac-police focuses on identifying in-cluster RBAC relationships. | 147 | | [kdigger](https://github.com/quarkslab/kdigger) | kdigger is a general-purpose context discovery tool for Kubernetes penetration testing. It does not attempt to be specific to managed K8s environments. | 148 | | [kubeletmein](https://github.com/4ARMED/kubeletmein) | kubeletmein _is_ specific to managed K8s environments. It's an utility to generate a kubeconfig file using the node's IAM credentials, to then use it in a compromised pod. | 149 | | [hardeneks](https://github.com/aws-samples/hardeneks) | hardeneks _is_ specific to managed K8s environments, but only for EKS. It identifies issues and lack of best practices inside of the cluster, and does not focus on cluster to cloud pivots. | 150 | 151 | ### What permissions does MKAT need to run? 152 | 153 | See [this page](./permissions.md) for a detailed list of the permissions MKAT needs to run. 154 | 155 | ## Roadmap 156 | 157 | We currently plan to: 158 | * Add a feature to identify EKS pods that are exposed through an AWS load balancer, through the [aws-load-balancer-controller](https://github.com/kubernetes-sigs/aws-load-balancer-controller) 159 | * Add support for GCP GKE 160 | * Allow scanning for additional types of cloud credentials 161 | 162 | ## Acknowledgements 163 | 164 | Thank you to Rami McCarthi and Mikail Tunç for their early testing and actionable feedback on MKAT! 165 | -------------------------------------------------------------------------------- /cmd/managed-kubernetes-auditing-toolkit/eks/find_secrets.go: -------------------------------------------------------------------------------- 1 | package eks 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/datadog/managed-kubernetes-auditing-toolkit/internal/utils" 7 | "github.com/datadog/managed-kubernetes-auditing-toolkit/pkg/managed-kubernetes-auditing-toolkit/eks/secrets" 8 | "github.com/fatih/color" 9 | "github.com/jedib0t/go-pretty/v6/table" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | func buildEksFindSecretsCommand() *cobra.Command { 14 | eksFindSecretsCommand := &cobra.Command{ 15 | Use: "find-secrets", 16 | Short: "Find hardcoded AWS secrets in your EKS cluster", 17 | Long: "find-secret will scan your EKS cluster for hardcoded AWS secrets in pod environment variables, configmaps and secrets", 18 | Example: "mkat eks find-secrets", 19 | DisableFlagsInUseLine: true, 20 | RunE: func(cmd *cobra.Command, args []string) error { 21 | return doFindSecretsCommand() 22 | }, 23 | } 24 | 25 | return eksFindSecretsCommand 26 | } 27 | 28 | func doFindSecretsCommand() error { 29 | detector := secrets.SecretsDetector{K8sClient: utils.K8sClient(), AwsClient: utils.AWSClient()} 30 | secrets, err := detector.FindSecrets() 31 | if err != nil { 32 | return err 33 | } 34 | 35 | if len(secrets) == 0 { 36 | log.Println("No hardcoded AWS secrets found in your AWS cluster") 37 | return nil 38 | } 39 | 40 | t := table.NewWriter() 41 | t.AppendHeader(table.Row{"Namespace", "Type", "Name", "Value"}) 42 | secretColor := color.New(color.BgRed, color.FgWhite, color.Bold) 43 | for _, secret := range secrets { 44 | t.AppendRow(table.Row{secret.Namespace, secret.Type, secret.Name, secretColor.Sprintf(secret.Value)}) 45 | } 46 | 47 | println(t.Render()) 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /cmd/managed-kubernetes-auditing-toolkit/eks/imds.go: -------------------------------------------------------------------------------- 1 | package eks 2 | 3 | import ( 4 | "log" 5 | "sync" 6 | 7 | "github.com/datadog/managed-kubernetes-auditing-toolkit/internal/utils" 8 | "github.com/datadog/managed-kubernetes-auditing-toolkit/pkg/managed-kubernetes-auditing-toolkit/eks/imds" 9 | "github.com/fatih/color" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | var successColor = color.New(color.BgBlack, color.FgGreen, color.Bold) 14 | var warningColor = color.New(color.BgRed, color.FgWhite, color.Bold) 15 | 16 | func buildTestImdsAccessCommand() *cobra.Command { 17 | eksFindSecretsCommand := &cobra.Command{ 18 | Use: "test-imds-access", 19 | Example: "mkat eks test-imds-access", 20 | Short: "Test if your EKS cluster allows pod access to the IMDS", 21 | Long: "test-imds-access will check if your EKS cluster allows pods to access the IMDS by running a pod and executing a curl command hitting the IMDS", 22 | DisableFlagsInUseLine: true, 23 | Run: func(cmd *cobra.Command, args []string) { 24 | doTestImdsAccessCommand() 25 | }, 26 | } 27 | 28 | return eksFindSecretsCommand 29 | } 30 | 31 | func doTestImdsAccessCommand() { 32 | tester := imds.ImdsTester{K8sClient: utils.K8sClient(), Namespace: "default"} 33 | log.Println("Testing if IMDSv1 and IMDSv2 are accessible from pods by creating a pod that attempts to access it") 34 | 35 | // We run the test for IMDSv1 and IMDSv2 in parallel 36 | var wg sync.WaitGroup 37 | wg.Add(2) 38 | go doTestImdsAccess(IMDSv1, &tester, &wg) 39 | go doTestImdsAccess(IMDSv2, &tester, &wg) 40 | wg.Wait() 41 | } 42 | 43 | type ImdsVersion string 44 | 45 | const ( 46 | IMDSv1 ImdsVersion = "IMDSv1" 47 | IMDSv2 ImdsVersion = "IMDSv2" 48 | ) 49 | 50 | func doTestImdsAccess(imdsVersion ImdsVersion, tester *imds.ImdsTester, wg *sync.WaitGroup) { 51 | var result *imds.ImdsTestResult 52 | var err error 53 | 54 | defer wg.Done() 55 | 56 | switch imdsVersion { 57 | case IMDSv1: 58 | result, err = tester.TestImdsV1Accessible() 59 | case IMDSv2: 60 | result, err = tester.TestImdsV2Accessible() 61 | default: 62 | panic("invalid IMDS version") 63 | } 64 | 65 | if err != nil { 66 | log.Printf("Unable to determine if %s is accessible in your cluster: %s\n", imdsVersion, err.Error()) 67 | return 68 | } 69 | 70 | if result.IsImdsAccessible { 71 | log.Printf("%s: %s\n", warningColor.Sprintf("%s is accessible", imdsVersion), result.ResultDescription) 72 | } else { 73 | description := "" 74 | if result.ResultDescription != "" { 75 | description = ": " + result.ResultDescription 76 | } 77 | log.Printf("%s %s%s\n", 78 | successColor.Sprintf("%s is not accessible", imdsVersion), 79 | "to pods in your cluster", 80 | description, 81 | ) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /cmd/managed-kubernetes-auditing-toolkit/eks/main.go: -------------------------------------------------------------------------------- 1 | package eks 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | 7 | "github.com/common-nighthawk/go-figure" 8 | "github.com/datadog/managed-kubernetes-auditing-toolkit/internal/utils" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var skipEksHostnameCheck bool 13 | 14 | func BuildEksSubcommand() *cobra.Command { 15 | eksCommand := &cobra.Command{ 16 | Use: "eks", 17 | Short: "Commands to audit your EKS cluster", 18 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 19 | figure.NewFigure("mkat", "", true).Print() 20 | println() 21 | if !skipEksHostnameCheck && !utils.IsEKS() { 22 | return errors.New("you do not seem to be connected to an EKS cluster. Connect to an EKS cluster and try again") 23 | } 24 | clusterName := utils.GetEKSClusterName() 25 | if clusterName != "" { 26 | log.Println("Connected to EKS cluster " + clusterName) 27 | } 28 | return nil 29 | }, 30 | } 31 | 32 | eksCommand.PersistentFlags().BoolVarP(&skipEksHostnameCheck, "skip-eks-hostname-check", "", false, "Don't check that the hostname of your current API server ends with .eks.amazonaws.com") 33 | eksCommand.AddCommand(buildEksRoleRelationshipsCommand()) 34 | eksCommand.AddCommand(buildEksFindSecretsCommand()) 35 | eksCommand.AddCommand(buildTestImdsAccessCommand()) 36 | 37 | return eksCommand 38 | } 39 | -------------------------------------------------------------------------------- /cmd/managed-kubernetes-auditing-toolkit/eks/role_relationships.go: -------------------------------------------------------------------------------- 1 | package eks 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "os" 8 | "strings" 9 | 10 | "github.com/awalterschulze/gographviz" 11 | "github.com/aws/aws-sdk-go-v2/aws/arn" 12 | "github.com/datadog/managed-kubernetes-auditing-toolkit/internal/utils" 13 | "github.com/datadog/managed-kubernetes-auditing-toolkit/pkg/managed-kubernetes-auditing-toolkit/eks/role_relationships" 14 | "github.com/jedib0t/go-pretty/v6/table" 15 | "github.com/jedib0t/go-pretty/v6/text" 16 | "github.com/spf13/cobra" 17 | "golang.org/x/exp/slices" 18 | "golang.org/x/term" 19 | ) 20 | 21 | // Command-line arguments 22 | var outputFormat string 23 | var outputFile string 24 | var eksClusterName string 25 | var showFullRoleArns bool 26 | 27 | // Output formats 28 | const ( 29 | CsvOutputFormat string = "csv" 30 | TextOutputFormat string = "text" 31 | DotOutputFormat string = "dot" 32 | ) 33 | 34 | var availableOutputFormats = []string{CsvOutputFormat, TextOutputFormat, DotOutputFormat} 35 | 36 | const DefaultOutputFormat = TextOutputFormat 37 | 38 | func buildEksRoleRelationshipsCommand() *cobra.Command { 39 | eksRoleRelationshipsCommand := &cobra.Command{ 40 | Use: "find-role-relationships", 41 | Example: "mkat eks find-role-relationships", 42 | Short: "Find relationships between your EKS service accounts and IAM roles", 43 | Long: "Analyzes your EKS cluster and finds all service accounts that can assume AWS roles, based on their trust policies ", 44 | DisableFlagsInUseLine: true, 45 | PreRunE: func(cmd *cobra.Command, args []string) error { 46 | if !slices.Contains(availableOutputFormats, outputFormat) { 47 | return fmt.Errorf("invalid output format %s", outputFormat) 48 | } 49 | return nil 50 | }, 51 | RunE: func(cmd *cobra.Command, args []string) error { 52 | cluster := utils.GetEKSClusterName() 53 | if cluster == "" { 54 | // If we cannot determine the EKS cluster name automatically, give the user a chance to specify it on the CLI 55 | cluster = eksClusterName 56 | } 57 | if cluster == "" { 58 | return errors.New("unable to determine your current EKS cluster name. Try specifying it explicitely with the --eks-cluster-name flag") 59 | } 60 | return doFindRoleRelationshipsCommand(cluster) 61 | }, 62 | } 63 | 64 | eksRoleRelationshipsCommand.Flags().StringVarP(&outputFormat, "output-format", "f", DefaultOutputFormat, "Output format. Supported formats: "+strings.Join(availableOutputFormats, ", ")) 65 | eksRoleRelationshipsCommand.Flags().StringVarP(&outputFile, "output-file", "o", "", "Output file. If not specified, output will be printed to stdout.") 66 | eksRoleRelationshipsCommand.Flags().StringVarP(&eksClusterName, "eks-cluster-name", "", "", "When the EKS cluster name cannot be automatically detected from your KubeConfig, specify this argument to pass the EKS cluster name of your current kubectl context") 67 | eksRoleRelationshipsCommand.Flags().BoolVarP(&showFullRoleArns, "show-full-role-arns", "", false, "Show full ARNs of roles instead of just the role name") 68 | return eksRoleRelationshipsCommand 69 | } 70 | 71 | // Actual logic implementing the "find-role-relationships" command 72 | func doFindRoleRelationshipsCommand(targetCluster string) error { 73 | resolver := role_relationships.EKSCluster{ 74 | K8sClient: utils.K8sClient(), 75 | AwsClient: utils.AWSClient(), 76 | Name: targetCluster, 77 | } 78 | err := resolver.AnalyzeRoleRelationships() 79 | if err != nil { 80 | log.Fatalf("unable to analyze cluster role relationships: %v", err) 81 | } 82 | 83 | output, err := getOutput(&resolver) 84 | if err != nil { 85 | return err 86 | } 87 | if outputFile != "" { 88 | log.Println("Writing " + strings.ToUpper(outputFormat) + " output to " + outputFile) 89 | return os.WriteFile(outputFile, []byte(output), 0644) 90 | } 91 | 92 | print(output) 93 | return nil 94 | } 95 | 96 | func getOutput(resolver *role_relationships.EKSCluster) (string, error) { 97 | switch outputFormat { 98 | case TextOutputFormat: 99 | return getTextOutput(resolver) 100 | case DotOutputFormat: 101 | return getDotOutput(resolver) 102 | case CsvOutputFormat: 103 | return getCsvOutput(resolver) 104 | default: 105 | return "", fmt.Errorf("unsupported output format %s", outputFormat) 106 | } 107 | } 108 | 109 | func getTextOutput(resolver *role_relationships.EKSCluster) (string, error) { 110 | t := table.NewWriter() 111 | if term.IsTerminal(0) { 112 | width, _, err := term.GetSize(0) 113 | if err == nil { 114 | t.SetAllowedRowLength(width) 115 | } 116 | } 117 | t.SetColumnConfigs([]table.ColumnConfig{ 118 | {Number: 1, AutoMerge: true, VAlign: text.VAlignMiddle}, 119 | {Number: 2, AutoMerge: true, VAlign: text.VAlignMiddle}, 120 | {Number: 3, AutoMerge: true, VAlign: text.VAlignMiddle}, 121 | }) 122 | t.AppendHeader(table.Row{"Namespace", "Service Account", "Pod", "Assumable Role", "Mechanism"}) 123 | var found = false 124 | for namespace, pods := range resolver.PodsByNamespace { 125 | for _, pod := range pods { 126 | if pod.ServiceAccount == nil || len(pod.ServiceAccount.AssumableRoles) == 0 { 127 | continue 128 | } 129 | for _, role := range pod.ServiceAccount.AssumableRoles { 130 | t.AppendRow([]interface{}{namespace, pod.ServiceAccount.Name, pod.Name, getRoleDisplayName(role.IAMRole), role.Reason}) 131 | found = true 132 | } 133 | } 134 | t.AppendSeparator() 135 | } 136 | if !found { 137 | return "No service accounts found that can assume AWS roles", nil 138 | } 139 | return t.Render(), nil 140 | } 141 | 142 | type Vertex struct { 143 | ID int 144 | Label string 145 | } 146 | 147 | func (v *Vertex) GetID() int { 148 | return v.ID 149 | } 150 | 151 | func getDotOutput(resolver *role_relationships.EKSCluster) (string, error) { 152 | graphAst, _ := gographviz.ParseString(`digraph G { }`) 153 | graphViz := gographviz.NewGraph() 154 | gographviz.Analyse(graphAst, graphViz) 155 | graphViz.AddAttr("G", "rankdir", "LR") 156 | graphViz.AddAttr("G", "splines", "polyline") 157 | graphViz.AddAttr("G", "ranksep", "1.2") 158 | graphViz.AddAttr("G", "nodesep", "0.8") 159 | graphViz.AddAttr("G", "outputorder", "edgesfirst") 160 | graphViz.AddAttr("G", "overlap", "false") 161 | graphViz.AddAttr("G", "newrank", "true") 162 | 163 | for namespace, pods := range resolver.PodsByNamespace { 164 | subgraph := fmt.Sprintf(` "cluster_%s" `, namespace) 165 | graphViz.AddSubGraph("G", subgraph, map[string]string{ 166 | "rank": "same", 167 | "label": fmt.Sprintf(`"%s"`, namespace), 168 | "color": "lightgrey", 169 | "style": "rounded", 170 | }) 171 | for _, pod := range pods { 172 | if pod.ServiceAccount == nil || len(pod.ServiceAccount.AssumableRoles) == 0 { 173 | continue 174 | } 175 | podLabel := fmt.Sprintf(` "Pod %s" `, pod.Name) 176 | graphViz.AddNode(subgraph, podLabel, map[string]string{ 177 | "fontname": "Helvetica", 178 | "shape": "box", 179 | "style": "filled", 180 | "fillcolor": "lightgrey", 181 | "fontsize": "12", 182 | }) 183 | for _, role := range pod.ServiceAccount.AssumableRoles { 184 | roleLabel := fmt.Sprintf(`"IAM role %s"`, getRoleName(role.IAMRole)) 185 | graphViz.AddNode("G", roleLabel, map[string]string{ 186 | "fontname": "Helvetica", 187 | "shape": "box", 188 | "style": "filled", 189 | "fillcolor": `"#BFEFFF"`, 190 | "fontsize": "12", 191 | }) 192 | graphViz.AddEdge(podLabel, roleLabel, true, map[string]string{ 193 | "fontname": "Helvetica", 194 | "color": "black", 195 | "penwidth": "1", 196 | "fontsize": "10", 197 | "weight": "2.0", 198 | }) 199 | } 200 | } 201 | } 202 | 203 | return graphViz.String(), nil 204 | } 205 | 206 | func getCsvOutput(resolver *role_relationships.EKSCluster) (string, error) { 207 | sb := new(strings.Builder) 208 | sb.WriteString("namespace,pod,service_account,role_arn,reason") 209 | for namespace, pods := range resolver.PodsByNamespace { 210 | for _, pod := range pods { 211 | if pod.ServiceAccount == nil || len(pod.ServiceAccount.AssumableRoles) == 0 { 212 | continue 213 | } 214 | for _, role := range pod.ServiceAccount.AssumableRoles { 215 | sb.WriteString(fmt.Sprintf( 216 | "%s,%s,%s,%s,%s", 217 | namespace, 218 | pod.Name, 219 | pod.ServiceAccount.Name, 220 | getRoleDisplayName(role.IAMRole), 221 | role.Reason, 222 | )) 223 | sb.WriteRune('\n') 224 | } 225 | } 226 | } 227 | 228 | return sb.String(), nil 229 | } 230 | 231 | func getRoleDisplayName(role *role_relationships.IAMRole) string { 232 | if showFullRoleArns { 233 | return role.Arn 234 | } 235 | return getRoleName(role) 236 | } 237 | 238 | func getRoleName(role *role_relationships.IAMRole) string { 239 | parsedArn, _ := arn.Parse(role.Arn) 240 | return strings.TrimPrefix(parsedArn.Resource, "role/") 241 | } 242 | -------------------------------------------------------------------------------- /cmd/managed-kubernetes-auditing-toolkit/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/datadog/managed-kubernetes-auditing-toolkit/cmd/managed-kubernetes-auditing-toolkit/eks" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | // BuildVersion is injected at compilation time 9 | var BuildVersion = "" 10 | 11 | var rootCmd = &cobra.Command{ 12 | Use: "mkat", 13 | DisableFlagsInUseLine: true, 14 | SilenceUsage: true, 15 | } 16 | 17 | func init() { 18 | rootCmd.CompletionOptions.DisableDefaultCmd = true 19 | 20 | rootCmd.AddCommand(eks.BuildEksSubcommand()) 21 | rootCmd.AddCommand(&cobra.Command{ 22 | Use: "version", 23 | Short: "Display the current CLI version", 24 | Run: func(cmd *cobra.Command, args []string) { 25 | println(BuildVersion) 26 | }, 27 | }) 28 | } 29 | 30 | func main() { 31 | rootCmd.Execute() 32 | } 33 | -------------------------------------------------------------------------------- /examples/demo-cluster/README.md: -------------------------------------------------------------------------------- 1 | This sample folder contains Terraform code to provision test resources in an EKS cluster, including pods, service accounts, and IAM roles configured for IAM Roles for Service Accounts. -------------------------------------------------------------------------------- /examples/demo-cluster/terraform/.gitignore: -------------------------------------------------------------------------------- 1 | .terraform 2 | .terraform.tfstate 3 | terraform.tfstate* 4 | -------------------------------------------------------------------------------- /examples/demo-cluster/terraform/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/aws" { 5 | version = "4.62.0" 6 | constraints = "~> 4.62.0" 7 | hashes = [ 8 | "h1:6x4fZWzzoUpQyIa6wl160ONU9o9IRmK6Hivt9zNFDug=", 9 | "zh:12059dc2b639797b9facb6397ac6aec563891634be8e5aadf3a457590c1147d4", 10 | "zh:1b3515d70b6998359d0a6d3b3c287940ab2e5c59cd02f95c7d9dab7df76e86b6", 11 | "zh:423a1d3afdb6b625f2e3b06770ef4324740d400ff1a0d6d566c87d3f841d74fc", 12 | "zh:58612b5a27d929dd1dff04d18d840b9cc59d45fed06247f0c2f87c1e5d3257d9", 13 | "zh:5b243cd2250dd097293e06c1cc85e805565194e53f594ccd070252c7af644f54", 14 | "zh:61ad9739e7d6fca8fddef269cb2ba7285f0632f5f27660755662550e1f69e4bb", 15 | "zh:6700d86f5bfcae8491c87a7769b211a079dbf6dfb325bde76bf407aca3e76ff4", 16 | "zh:67c7925f3b7ac1988c2aee8965b1f6f04738984cf8ae302b88215549793d14c1", 17 | "zh:686770264b907b3e4c75fd751f8ea717a7e393d2fbde0950c4703fa809e573f0", 18 | "zh:740236fda351a8f4976ddbd37e543c8d746a409e3a6aa290a8c5ff774b264455", 19 | "zh:88ace13281a344044624ed088125c30f1a803188bf95874d09ca7e95725d5727", 20 | "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", 21 | "zh:a4810a034f5def017607b0b079c7867c983da653928bd9f67edbc18575c0b629", 22 | "zh:e1c10e1641b5f17fec61910d6c3514e241f650ced84523f09cb16271a9a1e651", 23 | "zh:f63593ee2e01a2e1096ae9959fa43f0521114b3335f6440170f0d35d1969e8a2", 24 | ] 25 | } 26 | 27 | provider "registry.terraform.io/hashicorp/kubernetes" { 28 | version = "2.19.0" 29 | constraints = "~> 2.19.0" 30 | hashes = [ 31 | "h1:WXTbK59MHZVtikifTCvqpH/3TKMnj3MyQke4ymmUjlg=", 32 | "zh:028d346460de2d1d19b4c863dfc36be51c7bcd97d372b54a3a946bcb19f3f613", 33 | "zh:391d0b38c455437d0a2ab1beb6ce6e1230aa4160bbae11c58b2810b258b44280", 34 | "zh:40ea742f91b67f66e71d7091cfd40cc604528c4947651924bd6d8bd8d9793708", 35 | "zh:48a99d341c8ba3cadaafa7cb99c0f11999f5e23f5cfb0f8469b4e352d9116e74", 36 | "zh:4a5ade940eff267cbf7dcd52c1a7ac3999e7cc24996a409bd8b37bdb48a97f02", 37 | "zh:5063742016a8249a4be057b9cc0ef24a684ec76d0ae5463d4b07e9b2d21e047e", 38 | "zh:5d36b3a5662f840a6788f5e2a19d02139e87318feb3c5d82c7d076be1366fec4", 39 | "zh:75edd9960cb30e54ef7de1b7df2761a274f17d4d41f54e72f86b43f41af3eb6d", 40 | "zh:b85cadef3e6f25f1a10a617472bf5e8449decd61626733a1bc723de5edc08f64", 41 | "zh:dc565b17b4ea6dde6bd1b92bc37e5e850fcbf9400540eec00ad3d9552a76ac2e", 42 | "zh:deb665cc2123f2701aa3d653987b2ca35fb035a08a76a2382efb215c209f19a5", 43 | "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /examples/demo-cluster/terraform/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | objects = yamldecode(file("./objects.yaml")) 3 | } 4 | 5 | data "aws_eks_cluster" "cluster" { 6 | name = var.eks-cluster-name 7 | } 8 | data "aws_eks_cluster_auth" "cluster" { 9 | name = var.eks-cluster-name 10 | } 11 | provider "kubernetes" { 12 | host = data.aws_eks_cluster.cluster.endpoint 13 | cluster_ca_certificate = base64decode(data.aws_eks_cluster.cluster.certificate_authority.0.data) 14 | token = data.aws_eks_cluster_auth.cluster.token 15 | } 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/demo-cluster/terraform/objects.yaml: -------------------------------------------------------------------------------- 1 | namespaces: 2 | - microservices 3 | - kafka 4 | 5 | serviceAccounts: 6 | - name: rate-limiter-sa 7 | namespace: microservices 8 | role: rate-limiter-role 9 | - name: inventory-service-sa 10 | namespace: microservices 11 | role: inventory-service-role 12 | - name: kafka-proxy-sa 13 | namespace: kafka 14 | role: kafka-proxy-role 15 | - name: kafka-reducer-sa 16 | namespace: kafka 17 | role: kafka-reducer-role 18 | 19 | pods: 20 | - name: rate-limiter-1 21 | namespace: microservices 22 | serviceAccount: rate-limiter-sa 23 | - name: rate-limiter-2 24 | namespace: microservices 25 | serviceAccount: rate-limiter-sa 26 | - name: rate-limiter-3 27 | namespace: microservices 28 | serviceAccount: rate-limiter-sa 29 | - name: inventory-service 30 | namespace: microservices 31 | serviceAccount: inventory-service-sa 32 | - name: inventory-service-2 33 | namespace: microservices 34 | serviceAccount: inventory-service-sa 35 | - name: kafka-proxy 36 | namespace: kafka 37 | serviceAccount: kafka-proxy-sa 38 | - name: kafka-reducer 39 | namespace: kafka 40 | serviceAccount: kafka-reducer-sa 41 | 42 | roles: 43 | - name: rate-limiter-role 44 | allowedServiceAccounts: 45 | - namespace: microservices 46 | name: rate-limiter-sa 47 | - name: kafka-proxy-role 48 | allowedServiceAccounts: 49 | - namespace: kafka 50 | name: kafka-proxy-sa 51 | - name: inventory-service-role 52 | allowedServiceAccounts: 53 | - namespace: microservices 54 | name: inventory-service-sa 55 | - name: kafka-reducer-role 56 | allowedServiceAccounts: 57 | - namespace: kafka 58 | name: kafka-reducer-sa 59 | - name: s3-backup-role 60 | allowedServiceAccounts: 61 | - namespace: microservices 62 | name: inventory-service-sa 63 | - namespace: kafka 64 | name: kafka-proxy-sa -------------------------------------------------------------------------------- /examples/demo-cluster/terraform/pods.tf: -------------------------------------------------------------------------------- 1 | resource "kubernetes_namespace" "namespace" { 2 | for_each = toset(local.objects.namespaces) 3 | metadata { 4 | name = each.value 5 | } 6 | } 7 | 8 | resource "kubernetes_pod" "pod" { 9 | for_each = { for pod in local.objects.pods: "${pod.namespace}/${pod.name}" => pod } 10 | 11 | metadata { 12 | name = each.value.name 13 | namespace = each.value.namespace 14 | } 15 | 16 | spec { 17 | service_account_name = each.value.serviceAccount 18 | container { 19 | name = "main" 20 | image = "amazon/aws-cli:latest" 21 | command = ["sleep", "infinity"] 22 | } 23 | } 24 | 25 | depends_on = [kubernetes_namespace.namespace, kubernetes_service_account.service_account] 26 | } -------------------------------------------------------------------------------- /examples/demo-cluster/terraform/roles.tf: -------------------------------------------------------------------------------- 1 | data "aws_caller_identity" "current" {} 2 | 3 | module "iam_eks_role" { 4 | source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks" 5 | for_each = {for role in local.objects.roles: role.name => role} 6 | 7 | role_name = each.value.name 8 | 9 | role_policy_arns = { 10 | policy = "arn:aws:iam::aws:policy/job-function/ViewOnlyAccess" 11 | } 12 | 13 | oidc_providers = { 14 | one = { 15 | provider_arn = format("arn:aws:iam::%s:oidc-provider/%s", data.aws_caller_identity.current.account_id, replace(data.aws_eks_cluster.cluster.identity[0].oidc[0].issuer, "https://", "")) 16 | namespace_service_accounts = [for serviceAccount in each.value.allowedServiceAccounts: "${serviceAccount.namespace}:${serviceAccount.name}"] 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /examples/demo-cluster/terraform/secrets.tf: -------------------------------------------------------------------------------- 1 | resource "kubernetes_secret" "creds" { 2 | metadata { 3 | name = "kafka-proxy-aws" 4 | } 5 | data = { 6 | # Note: These are fictional keys 7 | aws_access_key_id = "AKIAZ3MSJV4WWNKWW5FG", 8 | aws_secret_key = "HP8lBRs8X50F/0nCAXqEPQ95+jlG/0pLdlNui2XF" 9 | } 10 | } -------------------------------------------------------------------------------- /examples/demo-cluster/terraform/serviceaccounts.tf: -------------------------------------------------------------------------------- 1 | 2 | 3 | resource "kubernetes_service_account" "service_account" { 4 | for_each = { for serviceAccount in local.objects.serviceAccounts: serviceAccount.name => serviceAccount } 5 | 6 | metadata { 7 | name = each.value.name 8 | namespace = each.value.namespace 9 | annotations = { 10 | "eks.amazonaws.com/role-arn" = module.iam_eks_role[each.value.role].iam_role_arn 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /examples/demo-cluster/terraform/variables.tf: -------------------------------------------------------------------------------- 1 | variable "eks-cluster-name" { 2 | description = "Name of the EKS cluster to provision the resources into" 3 | } -------------------------------------------------------------------------------- /examples/demo-cluster/terraform/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "~> 4.62.0" 6 | } 7 | kubernetes = { 8 | source = "hashicorp/kubernetes" 9 | version = "~> 2.19.0" 10 | } 11 | } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /examples/irsa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataDog/managed-kubernetes-auditing-toolkit/0f2f5f95d702ed7bf44b4303300cbc8cb35fa135/examples/irsa.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/datadog/managed-kubernetes-auditing-toolkit 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/awalterschulze/gographviz v2.0.3+incompatible 7 | github.com/aws/aws-sdk-go-v2 v1.23.1 8 | github.com/aws/aws-sdk-go-v2/config v1.25.6 9 | github.com/aws/aws-sdk-go-v2/service/eks v1.34.1 10 | github.com/aws/aws-sdk-go-v2/service/iam v1.27.4 11 | github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be 12 | github.com/fatih/color v1.15.0 13 | github.com/hashicorp/go-version v1.6.0 14 | github.com/jedib0t/go-pretty/v6 v6.4.6 15 | github.com/spf13/cobra v1.6.1 16 | github.com/stretchr/testify v1.8.0 17 | golang.org/x/exp v0.0.0-20230321023759-10a507213a29 18 | golang.org/x/term v0.5.0 19 | k8s.io/api v0.26.2 20 | k8s.io/apimachinery v0.26.2 21 | k8s.io/client-go v0.26.2 22 | ) 23 | 24 | require ( 25 | github.com/aws/aws-sdk-go-v2/credentials v1.16.5 // indirect 26 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.5 // indirect 27 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.4 // indirect 28 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.4 // indirect 29 | github.com/aws/aws-sdk-go-v2/internal/ini v1.7.1 // indirect 30 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.1 // indirect 31 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.4 // indirect 32 | github.com/aws/aws-sdk-go-v2/service/sso v1.17.4 // indirect 33 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.20.2 // indirect 34 | github.com/aws/aws-sdk-go-v2/service/sts v1.25.5 // indirect 35 | github.com/aws/smithy-go v1.17.0 // indirect 36 | github.com/davecgh/go-spew v1.1.1 // indirect 37 | github.com/emicklei/go-restful/v3 v3.9.0 // indirect 38 | github.com/go-logr/logr v1.2.3 // indirect 39 | github.com/go-openapi/jsonpointer v0.19.5 // indirect 40 | github.com/go-openapi/jsonreference v0.20.0 // indirect 41 | github.com/go-openapi/swag v0.19.14 // indirect 42 | github.com/gogo/protobuf v1.3.2 // indirect 43 | github.com/golang/protobuf v1.5.2 // indirect 44 | github.com/google/gnostic v0.5.7-v3refs // indirect 45 | github.com/google/go-cmp v0.5.9 // indirect 46 | github.com/google/gofuzz v1.1.0 // indirect 47 | github.com/imdario/mergo v0.3.6 // indirect 48 | github.com/inconshreveable/mousetrap v1.0.1 // indirect 49 | github.com/jmespath/go-jmespath v0.4.0 // indirect 50 | github.com/josharian/intern v1.0.0 // indirect 51 | github.com/json-iterator/go v1.1.12 // indirect 52 | github.com/mailru/easyjson v0.7.6 // indirect 53 | github.com/mattn/go-colorable v0.1.13 // indirect 54 | github.com/mattn/go-isatty v0.0.17 // indirect 55 | github.com/mattn/go-runewidth v0.0.13 // indirect 56 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 57 | github.com/modern-go/reflect2 v1.0.2 // indirect 58 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 59 | github.com/pmezard/go-difflib v1.0.0 // indirect 60 | github.com/rivo/uniseg v0.2.0 // indirect 61 | github.com/spf13/pflag v1.0.5 // indirect 62 | golang.org/x/net v0.7.0 // indirect 63 | golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2 // indirect 64 | golang.org/x/sys v0.6.0 // indirect 65 | golang.org/x/text v0.7.0 // indirect 66 | golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect 67 | google.golang.org/appengine v1.6.7 // indirect 68 | google.golang.org/protobuf v1.28.1 // indirect 69 | gopkg.in/inf.v0 v0.9.1 // indirect 70 | gopkg.in/yaml.v2 v2.4.0 // indirect 71 | gopkg.in/yaml.v3 v3.0.1 // indirect 72 | k8s.io/klog/v2 v2.80.1 // indirect 73 | k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect 74 | k8s.io/utils v0.0.0-20221107191617-1a15be271d1d // indirect 75 | sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect 76 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect 77 | sigs.k8s.io/yaml v1.3.0 // indirect 78 | ) 79 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 8 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 9 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 10 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 11 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 12 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= 13 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= 14 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= 15 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= 16 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 17 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 18 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 19 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 20 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= 21 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= 22 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 23 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 24 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 25 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 26 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 27 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= 28 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 29 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 30 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 31 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 32 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 33 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 34 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 35 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 36 | github.com/awalterschulze/gographviz v2.0.3+incompatible h1:9sVEXJBJLwGX7EQVhLm2elIKCm7P2YHFC8v6096G09E= 37 | github.com/awalterschulze/gographviz v2.0.3+incompatible/go.mod h1:GEV5wmg4YquNw7v1kkyoX9etIk8yVmXj+AkDHuuETHs= 38 | github.com/aws/aws-sdk-go-v2 v1.23.1 h1:qXaFsOOMA+HsZtX8WoCa+gJnbyW7qyFFBlPqvTSzbaI= 39 | github.com/aws/aws-sdk-go-v2 v1.23.1/go.mod h1:i1XDttT4rnf6vxc9AuskLc6s7XBee8rlLilKlc03uAA= 40 | github.com/aws/aws-sdk-go-v2/config v1.25.6 h1:p7b0sR6lHVNNOK/dE4xZgq2R+NNFRjtAXy8WNE6jbpo= 41 | github.com/aws/aws-sdk-go-v2/config v1.25.6/go.mod h1:E/nt0ERX9ZX2RCcJWBax94jFn738UERvjSn4R3msEeQ= 42 | github.com/aws/aws-sdk-go-v2/credentials v1.16.5 h1:oJz7X2VzKl8Y9pX7Fa5sIy4+3OnknF+Ne0KYu7DCoQQ= 43 | github.com/aws/aws-sdk-go-v2/credentials v1.16.5/go.mod h1:2HvVzcP9ih6XR66omXIsgWjtolkL0MlQVqPcK3nXK+E= 44 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.5 h1:KehRNiVzIfAcj6gw98zotVbb/K67taJE0fkfgM6vzqU= 45 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.5/go.mod h1:VhnExhw6uXy9QzetvpXDolo1/hjhx4u9qukBGkuUwjs= 46 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.4 h1:LAm3Ycm9HJfbSCd5I+wqC2S9Ej7FPrgr5CQoOljJZcE= 47 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.4/go.mod h1:xEhvbJcyUf/31yfGSQBe01fukXwXJ0gxDp7rLfymWE0= 48 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.4 h1:4GV0kKZzUxiWxSVpn/9gwR0g21NF1Jsyduzo9rHgC/Q= 49 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.4/go.mod h1:dYvTNAggxDZy6y1AF7YDwXsPuHFy/VNEpEI/2dWK9IU= 50 | github.com/aws/aws-sdk-go-v2/internal/ini v1.7.1 h1:uR9lXYjdPX0xY+NhvaJ4dD8rpSRz5VY81ccIIoNG+lw= 51 | github.com/aws/aws-sdk-go-v2/internal/ini v1.7.1/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= 52 | github.com/aws/aws-sdk-go-v2/service/eks v1.34.1 h1:lcpAUbLg8uZHGuZxOwm3TqSMt2LV/XTevPkGCu78PRk= 53 | github.com/aws/aws-sdk-go-v2/service/eks v1.34.1/go.mod h1:DInudKNZjEy7SJ0KfRh4VxaqY04B52Lq2+QRuvObfNQ= 54 | github.com/aws/aws-sdk-go-v2/service/iam v1.27.4 h1:W7aZ6WYk/R3kGhBbD6tAVwzYav8k0JQCGhEE+kXKl+k= 55 | github.com/aws/aws-sdk-go-v2/service/iam v1.27.4/go.mod h1:LklzfZoa7bL/NdhOzoaRtqSLGhu5j+GqE/9WoOQGFKY= 56 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.1 h1:rpkF4n0CyFcrJUG/rNNohoTmhtWlFTRI4BsZOh9PvLs= 57 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.1/go.mod h1:l9ymW25HOqymeU2m1gbUQ3rUIsTwKs8gYHXkqDQUhiI= 58 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.4 h1:rdovz3rEu0vZKbzoMYPTehp0E8veoE9AyfzqCr5Eeao= 59 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.4/go.mod h1:aYCGNjyUCUelhofxlZyj63srdxWUSsBSGg5l6MCuXuE= 60 | github.com/aws/aws-sdk-go-v2/service/sso v1.17.4 h1:WSMiDIMaDGyIiXwruNITU0IJF0d0foXwjxpxRylamqQ= 61 | github.com/aws/aws-sdk-go-v2/service/sso v1.17.4/go.mod h1:oA6VjNsLll2eVuUoF2D+CMyORgNzPEW/3PyUdq6WQjI= 62 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.20.2 h1:GsrlsvTPBNxHvE3KBCwUMnR76MTO/6qnnO1ILSUOpTA= 63 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.20.2/go.mod h1:hHL974p5auvXlZPIjJTblXJpbkfK4klBczlsEaMCGVY= 64 | github.com/aws/aws-sdk-go-v2/service/sts v1.25.5 h1:jwpmP8FnZPdpmJ8hkximoPQFGCUzfIekccwkxlfVfHQ= 65 | github.com/aws/aws-sdk-go-v2/service/sts v1.25.5/go.mod h1:feTnm2Tk/pJxdX+eooEsxvlvTWBvDm6CasRZ+JOs2IY= 66 | github.com/aws/smithy-go v1.17.0 h1:wWJD7LX6PBV6etBUwO0zElG0nWN9rUhp0WdYeHSHAaI= 67 | github.com/aws/smithy-go v1.17.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= 68 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 69 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 70 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 71 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 72 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 73 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 74 | github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be h1:J5BL2kskAlV9ckgEsNQXscjIaLiOYiZ75d4e94E6dcQ= 75 | github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w= 76 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 77 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 78 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 79 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 80 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 81 | github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= 82 | github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= 83 | github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 84 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 85 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 86 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 87 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 88 | github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= 89 | github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= 90 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 91 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 92 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 93 | github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 94 | github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= 95 | github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 96 | github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 97 | github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= 98 | github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 99 | github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= 100 | github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= 101 | github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 102 | github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng= 103 | github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= 104 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 105 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 106 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 107 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 108 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 109 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 110 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 111 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 112 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 113 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 114 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 115 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 116 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 117 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 118 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 119 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 120 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 121 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 122 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 123 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 124 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 125 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 126 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 127 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 128 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 129 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 130 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 131 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 132 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 133 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 134 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 135 | github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= 136 | github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= 137 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 138 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 139 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 140 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 141 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 142 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 143 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 144 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 145 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 146 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 147 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 148 | github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= 149 | github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 150 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 151 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 152 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 153 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 154 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 155 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 156 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 157 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 158 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 159 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 160 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 161 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 162 | github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= 163 | github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 164 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 165 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 166 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 167 | github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= 168 | github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 169 | github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= 170 | github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 171 | github.com/jedib0t/go-pretty/v6 v6.4.6 h1:v6aG9h6Uby3IusSSEjHaZNXpHFhzqMmjXcPq1Rjl9Jw= 172 | github.com/jedib0t/go-pretty/v6 v6.4.6/go.mod h1:Ndk3ase2CkQbXLLNf5QDHoYb6J9WtVfmHZu9n8rk2xs= 173 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 174 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 175 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 176 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 177 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 178 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 179 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 180 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 181 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 182 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 183 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 184 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 185 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 186 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 187 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 188 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 189 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 190 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 191 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 192 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 193 | github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= 194 | github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 195 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 196 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 197 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 198 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= 199 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 200 | github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= 201 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 202 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 203 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 204 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 205 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 206 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 207 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 208 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 209 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 210 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 211 | github.com/onsi/ginkgo/v2 v2.4.0 h1:+Ig9nvqgS5OBSACXNk15PLdp0U9XPYROt9CFzVdFGIs= 212 | github.com/onsi/gomega v1.23.0 h1:/oxKu9c2HVap+F3PfKort2Hw5DEU+HGlW8n+tguWsys= 213 | github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= 214 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 215 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 216 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 217 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 218 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 219 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 220 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 221 | github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= 222 | github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= 223 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 224 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 225 | github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= 226 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 227 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 228 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 229 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 230 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 231 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 232 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 233 | github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 234 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 235 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 236 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 237 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 238 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 239 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 240 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 241 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 242 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 243 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 244 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 245 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 246 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 247 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 248 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 249 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 250 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 251 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 252 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 253 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 254 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 255 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 256 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 257 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 258 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 259 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 260 | golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug= 261 | golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= 262 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 263 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 264 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 265 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 266 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 267 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 268 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 269 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 270 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 271 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 272 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 273 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 274 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 275 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 276 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 277 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 278 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 279 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 280 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 281 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 282 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 283 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 284 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 285 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 286 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 287 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 288 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 289 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 290 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 291 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 292 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 293 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 294 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 295 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 296 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 297 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 298 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 299 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 300 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 301 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 302 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 303 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 304 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 305 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 306 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 307 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 308 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 309 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 310 | golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= 311 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 312 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 313 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 314 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 315 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 316 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 317 | golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2 h1:+jnHzr9VPj32ykQVai5DNahi9+NSp7yYuCsl5eAQtL0= 318 | golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= 319 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 320 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 321 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 322 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 323 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 324 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 325 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 326 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 327 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 328 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 329 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 330 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 331 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 332 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 333 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 334 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 335 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 336 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 337 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 338 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 339 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 340 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 341 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 342 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 343 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 344 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 345 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 346 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 347 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 348 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 349 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 350 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 351 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 352 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 353 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 354 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 355 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 356 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 357 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 358 | golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= 359 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 360 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 361 | golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= 362 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 363 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 364 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 365 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 366 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 367 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 368 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 369 | golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= 370 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 371 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 372 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 373 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 374 | golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= 375 | golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 376 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 377 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 378 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 379 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 380 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 381 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 382 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 383 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 384 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 385 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 386 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 387 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 388 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 389 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 390 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 391 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 392 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 393 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 394 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 395 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 396 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 397 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 398 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 399 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 400 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 401 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 402 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 403 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 404 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 405 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 406 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 407 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 408 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 409 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 410 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 411 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 412 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 413 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 414 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 415 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 416 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 417 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 418 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 419 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 420 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 421 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 422 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 423 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 424 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 425 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 426 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 427 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 428 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 429 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 430 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 431 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 432 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 433 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 434 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 435 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 436 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= 437 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 438 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 439 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 440 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 441 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 442 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 443 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 444 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= 445 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 446 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 447 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 448 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 449 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 450 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 451 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 452 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 453 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 454 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 455 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 456 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 457 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 458 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 459 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 460 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 461 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 462 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 463 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 464 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 465 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 466 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 467 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 468 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 469 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 470 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 471 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 472 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 473 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 474 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 475 | google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 476 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 477 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 478 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 479 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 480 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 481 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 482 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 483 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 484 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 485 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 486 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 487 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 488 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 489 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 490 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 491 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 492 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 493 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 494 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 495 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 496 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 497 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 498 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 499 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 500 | google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= 501 | google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 502 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 503 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 504 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 505 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 506 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 507 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 508 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 509 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 510 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 511 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 512 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 513 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 514 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 515 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 516 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 517 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 518 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 519 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 520 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 521 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 522 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 523 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 524 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 525 | k8s.io/api v0.26.2 h1:dM3cinp3PGB6asOySalOZxEG4CZ0IAdJsrYZXE/ovGQ= 526 | k8s.io/api v0.26.2/go.mod h1:1kjMQsFE+QHPfskEcVNgL3+Hp88B80uj0QtSOlj8itU= 527 | k8s.io/apimachinery v0.26.2 h1:da1u3D5wfR5u2RpLhE/ZtZS2P7QvDgLZTi9wrNZl/tQ= 528 | k8s.io/apimachinery v0.26.2/go.mod h1:ats7nN1LExKHvJ9TmwootT00Yz05MuYqPXEXaVeOy5I= 529 | k8s.io/client-go v0.26.2 h1:s1WkVujHX3kTp4Zn4yGNFK+dlDXy1bAAkIl+cFAiuYI= 530 | k8s.io/client-go v0.26.2/go.mod h1:u5EjOuSyBa09yqqyY7m3abZeovO/7D/WehVVlZ2qcqU= 531 | k8s.io/klog/v2 v2.80.1 h1:atnLQ121W371wYYFawwYx1aEY2eUfs4l3J72wtgAwV4= 532 | k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= 533 | k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 h1:+70TFaan3hfJzs+7VK2o+OGxg8HsuBr/5f6tVAjDu6E= 534 | k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280/go.mod h1:+Axhij7bCpeqhklhUTe3xmOn6bWxolyZEeyaFpjGtl4= 535 | k8s.io/utils v0.0.0-20221107191617-1a15be271d1d h1:0Smp/HP1OH4Rvhe+4B8nWGERtlqAGSftbSbbmm45oFs= 536 | k8s.io/utils v0.0.0-20221107191617-1a15be271d1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 537 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 538 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 539 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 540 | sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k= 541 | sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= 542 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= 543 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= 544 | sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= 545 | sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= 546 | -------------------------------------------------------------------------------- /internal/aws/iam_evaluation/authorization.go: -------------------------------------------------------------------------------- 1 | package iam_evaluation 2 | 3 | type AuthorizationDecision string 4 | 5 | const ( 6 | AuthorizationDecisionAllow AuthorizationDecision = "ALLOW" 7 | AuthorizationDecisionDeny AuthorizationDecision = "DENY" 8 | ) 9 | 10 | type AuthorizationContext struct { 11 | Action string 12 | Principal *Principal 13 | ContextKeys map[string]string 14 | } 15 | 16 | type AuthorizationResult struct { 17 | Decision AuthorizationDecision 18 | } 19 | 20 | var ( 21 | AuthorizationResultDeny = AuthorizationResult{Decision: AuthorizationDecisionDeny} 22 | AuthorizationResultAllow = AuthorizationResult{Decision: AuthorizationDecisionAllow} 23 | AuthorizationResultNoDecision = AuthorizationResult{Decision: ""} 24 | ) 25 | -------------------------------------------------------------------------------- /internal/aws/iam_evaluation/condition.go: -------------------------------------------------------------------------------- 1 | package iam_evaluation 2 | 3 | import ( 4 | "github.com/datadog/managed-kubernetes-auditing-toolkit/internal/utils" 5 | "path/filepath" 6 | "strings" 7 | ) 8 | 9 | type Condition struct { 10 | Key string 11 | Operator string 12 | AllowedValues []string 13 | } 14 | 15 | var ConditionOperators = map[string]func(string, string) bool{ 16 | "stringequals": func(input string, value string) bool { 17 | return input == value 18 | }, 19 | "stringlike": func(input string, pattern string) bool { 20 | matches, err := filepath.Match(pattern, input) 21 | return matches && err == nil 22 | }, 23 | } 24 | 25 | func (m *Condition) Matches(context *AuthorizationContext) bool { 26 | operatorFunc, found := ConditionOperators[strings.ToLower(m.Operator)] 27 | contextKeysMap := utils.NewCaseInsensitiveMap(&context.ContextKeys) 28 | if !found { 29 | // unknown operator, the condition cannot match 30 | return false 31 | } 32 | for _, allowedValue := range m.AllowedValues { 33 | contextKey, hasContextKey := contextKeysMap.Get(m.Key) 34 | if hasContextKey && operatorFunc(contextKey, allowedValue) { 35 | return true 36 | } 37 | } 38 | 39 | return false 40 | } 41 | -------------------------------------------------------------------------------- /internal/aws/iam_evaluation/condition_test.go: -------------------------------------------------------------------------------- 1 | package iam_evaluation 2 | 3 | import "testing" 4 | 5 | func TestConditionStringEquals(t *testing.T) { 6 | scenarios := []struct { 7 | Name string 8 | Condition *Condition 9 | AuthorizationContext *AuthorizationContext 10 | ShouldMatch bool 11 | }{ 12 | { 13 | "unknown operator should not match", 14 | &Condition{ 15 | Key: "foo", 16 | Operator: "OperatorThatDoesNotExist", 17 | AllowedValues: []string{"bar"}, 18 | }, 19 | &AuthorizationContext{ContextKeys: map[string]string{}}, 20 | false, 21 | }, 22 | { 23 | "simple string equals", 24 | &Condition{ 25 | Key: "foo", 26 | Operator: "StringEquals", 27 | AllowedValues: []string{"bar"}, 28 | }, 29 | &AuthorizationContext{ContextKeys: map[string]string{"foo": "bar"}}, 30 | true, 31 | }, 32 | { 33 | "simple string equals with no match", 34 | &Condition{ 35 | Key: "foo", 36 | Operator: "StringEquals", 37 | AllowedValues: []string{"baz"}, 38 | }, 39 | &AuthorizationContext{ContextKeys: map[string]string{"foo": "bar"}}, 40 | false, 41 | }, 42 | { 43 | "simple string equals with multiple value should OR them together", 44 | &Condition{ 45 | Key: "foo", 46 | Operator: "StringEquals", 47 | AllowedValues: []string{"baz", "bar"}, 48 | }, 49 | &AuthorizationContext{ContextKeys: map[string]string{"foo": "bar"}}, 50 | true, 51 | }, 52 | { 53 | "condition keys are not case sensitive, per https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html", 54 | &Condition{ 55 | Key: "AWS:SourceIp", 56 | Operator: "StringEquals", 57 | AllowedValues: []string{"foo"}, 58 | }, 59 | &AuthorizationContext{ContextKeys: map[string]string{"aws:sourceip": "foo"}}, 60 | true, 61 | }, 62 | } 63 | 64 | for _, scenario := range scenarios { 65 | t.Run(scenario.Name, func(t *testing.T) { 66 | result := scenario.Condition.Matches(scenario.AuthorizationContext) 67 | if result && !scenario.ShouldMatch { 68 | t.Errorf("condition matched, expected it NOT to match") 69 | } else if !result && scenario.ShouldMatch { 70 | t.Errorf("condition did NOT match, expected it to match") 71 | } 72 | }) 73 | } 74 | } 75 | 76 | func TestConditionStringLike(t *testing.T) { 77 | scenarios := []struct { 78 | Name string 79 | Condition *Condition 80 | AuthorizationContext *AuthorizationContext 81 | ShouldMatch bool 82 | }{ 83 | { 84 | "simple string like with no wildcard should match", 85 | &Condition{ 86 | Key: "foo", 87 | Operator: "StringLike", 88 | AllowedValues: []string{"bar"}, 89 | }, 90 | &AuthorizationContext{ContextKeys: map[string]string{"foo": "bar"}}, 91 | true, 92 | }, 93 | { 94 | "simple string like with wildcard", 95 | &Condition{ 96 | Key: "foo", 97 | Operator: "StringLike", 98 | AllowedValues: []string{"b*"}, 99 | }, 100 | &AuthorizationContext{ContextKeys: map[string]string{"foo": "bar"}}, 101 | true, 102 | }, 103 | { 104 | "simple string like with wildcard not matching", 105 | &Condition{ 106 | Key: "foo", 107 | Operator: "StringLike", 108 | AllowedValues: []string{"b*"}, 109 | }, 110 | &AuthorizationContext{ContextKeys: map[string]string{"foo": "nope"}}, 111 | false, 112 | }, 113 | { 114 | "simple string like with multiple value should OR them together", 115 | &Condition{ 116 | Key: "foo", 117 | Operator: "StringLike", 118 | AllowedValues: []string{"a*", "b*"}, 119 | }, 120 | &AuthorizationContext{ContextKeys: map[string]string{"foo": "bar"}}, 121 | true, 122 | }, 123 | { 124 | "simple string like with wildcard should match", 125 | &Condition{ 126 | Key: "foo", 127 | Operator: "StringLike", 128 | AllowedValues: []string{"*"}, 129 | }, 130 | &AuthorizationContext{ContextKeys: map[string]string{"foo": "bar"}}, 131 | true, 132 | }, 133 | { 134 | "simple string like with wildcard should not match missing key", 135 | &Condition{ 136 | Key: "foo", 137 | Operator: "StringLike", 138 | AllowedValues: []string{"*"}, 139 | }, 140 | &AuthorizationContext{ContextKeys: map[string]string{"nope": "nope"}}, 141 | false, 142 | }, 143 | } 144 | 145 | for _, scenario := range scenarios { 146 | t.Run(scenario.Name, func(t *testing.T) { 147 | result := scenario.Condition.Matches(scenario.AuthorizationContext) 148 | if result && !scenario.ShouldMatch { 149 | t.Errorf("condition matched, expected it NOT to match") 150 | } else if !result && scenario.ShouldMatch { 151 | t.Errorf("condition did NOT match, expected it to match") 152 | } 153 | }) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /internal/aws/iam_evaluation/policy.go: -------------------------------------------------------------------------------- 1 | package iam_evaluation 2 | 3 | type Policy struct { 4 | Statements []*PolicyStatement 5 | } 6 | 7 | func (m *Policy) Authorize(context *AuthorizationContext) *AuthorizationResult { 8 | willAllow := false 9 | 10 | for _, statement := range m.Statements { 11 | decision := *statement.Authorize(context) 12 | if decision == AuthorizationResultDeny { 13 | return &AuthorizationResultDeny // explicit deny, overwriting any previous allow statement 14 | } else if decision == AuthorizationResultAllow { 15 | willAllow = true 16 | } 17 | } 18 | 19 | if willAllow { 20 | return &AuthorizationResultAllow 21 | } 22 | 23 | return &AuthorizationResultDeny // implicit deny 24 | } 25 | -------------------------------------------------------------------------------- /internal/aws/iam_evaluation/policy_parser.go: -------------------------------------------------------------------------------- 1 | package iam_evaluation 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | type rawStatement struct { 10 | Effect string `json:"Effect"` 11 | Action interface{} `json:"Action"` 12 | Principal interface{} `json:"Principal"` 13 | Condition map[string]map[string]interface{} `json:"Condition"` 14 | } 15 | 16 | type rawPolicy struct { 17 | Statement []rawStatement `json:"Statement"` 18 | } 19 | 20 | func ParseRoleTrustPolicy(policy string) (*Policy, error) { 21 | var rawPolicy rawPolicy 22 | resultPolicy := Policy{} 23 | err := json.Unmarshal([]byte(policy), &rawPolicy) 24 | if err != nil { 25 | return nil, fmt.Errorf("unable to parse role trust policy from JSON: %v", err) 26 | } 27 | for _, rawStatement := range rawPolicy.Statement { 28 | statement, err := parsePolicyStatement(&rawStatement) 29 | if err != nil { 30 | return nil, err 31 | } 32 | resultPolicy.Statements = append(resultPolicy.Statements, statement) 33 | } 34 | 35 | return &resultPolicy, nil 36 | } 37 | 38 | func parsePolicyStatement(rawStatement *rawStatement) (*PolicyStatement, error) { 39 | 40 | var statement PolicyStatement 41 | effect, err := parseStatementEffect(rawStatement.Effect) 42 | if err != nil { 43 | return nil, err 44 | } 45 | statement.Effect = effect 46 | 47 | actions, err := ensureStringArray(rawStatement.Action) 48 | if err != nil { 49 | return nil, err 50 | } 51 | statement.AllowedActions = actions 52 | 53 | principals, err := parsePrincipals(rawStatement.Principal) 54 | if err != nil { 55 | return nil, err 56 | } 57 | statement.AllowedPrincipals = principals 58 | 59 | conditions, err := parseConditions(rawStatement.Condition) 60 | if err != nil { 61 | return nil, err 62 | } 63 | statement.Conditions = conditions 64 | 65 | return &statement, nil 66 | 67 | } 68 | 69 | func parseConditions(rawConditions map[string]map[string]interface{}) ([]*Condition, error) { 70 | result := []*Condition{} 71 | for conditionOperator, conditionValues := range rawConditions { 72 | conditions, err := parseSingleCondition(conditionOperator, conditionValues) 73 | if err != nil { 74 | return nil, err 75 | } 76 | result = append(result, conditions...) 77 | } 78 | return result, nil 79 | } 80 | 81 | func parseSingleCondition(operator string, values map[string]interface{}) ([]*Condition, error) { 82 | conditions := []*Condition{} 83 | for conditionKey, conditionValues := range values { 84 | 85 | values, err := ensureStringArray(conditionValues) 86 | if err != nil { 87 | return nil, err 88 | } 89 | conditions = append(conditions, &Condition{ 90 | Operator: operator, 91 | Key: conditionKey, 92 | AllowedValues: values, 93 | }) 94 | 95 | } 96 | return conditions, nil 97 | } 98 | 99 | func parsePrincipals(principals interface{}) ([]*Principal, error) { 100 | 101 | // Case 1: principals is a string and contains "*" 102 | // Case 2: principals is a map, each entry of the form 103 | // ("AWS" | "Federated" | "Service" | "CanonicalUser") : 104 | // [, , ...] 105 | 106 | switch principals := principals.(type) { 107 | case string: 108 | if principals == "*" { 109 | return []*Principal{{Type: PrincipalTypeUnknown, ID: "*"}}, nil 110 | } 111 | return nil, fmt.Errorf("invalid principal: %s", principals) 112 | case map[string]interface{}: 113 | results := []*Principal{} 114 | for principalType, principalValue := range principals { 115 | result, err := parseSinglePrincipal(principalType, principalValue) 116 | if err != nil { 117 | return nil, err 118 | } 119 | results = append(results, result...) 120 | } 121 | return results, nil 122 | default: 123 | return nil, fmt.Errorf("invalid principal: %v", principals) 124 | } 125 | } 126 | 127 | func parseSinglePrincipal(rawPrincipalType string, principalID interface{}) ([]*Principal, error) { 128 | types := map[string]PrincipalType{ 129 | "aws": PrincipalTypeAWS, 130 | "federated": PrincipalTypeFederated, 131 | "service": PrincipalTypeService, 132 | "canonicaluser": PrincipalTypeCanonicalUser, 133 | } 134 | principalType, ok := types[strings.ToLower(rawPrincipalType)] 135 | if !ok { 136 | return nil, fmt.Errorf("invalid principal type: %s", rawPrincipalType) 137 | } 138 | values, err := ensureStringArray(principalID) 139 | if err != nil { 140 | return nil, fmt.Errorf("invalid principal value: %v", principalID) 141 | } 142 | 143 | principals := []*Principal{} 144 | for _, value := range values { 145 | principals = append(principals, &Principal{Type: principalType, ID: value}) 146 | } 147 | return principals, nil 148 | } 149 | 150 | func ensureStringArray(stringOrArray interface{}) ([]string, error) { 151 | switch value := stringOrArray.(type) { 152 | case string: 153 | return []string{value}, nil 154 | case []string: 155 | return stringOrArray.([]string), nil 156 | case []interface{}: 157 | values := make([]string, len(value)) 158 | for i, v := range value { 159 | stringValue, ok := v.(string) 160 | if !ok { 161 | return nil, fmt.Errorf("value cannot be converted to string array: %v", stringOrArray) 162 | } 163 | values[i] = stringValue 164 | } 165 | return values, nil 166 | default: 167 | return nil, fmt.Errorf("value cannot be converted to string array: %v", stringOrArray) 168 | } 169 | } 170 | 171 | func parseStatementEffect(rawEffect string) (AuthorizationDecision, error) { 172 | switch strings.ToLower(rawEffect) { 173 | case "allow": 174 | return AuthorizationDecisionAllow, nil 175 | case "deny": 176 | return AuthorizationDecisionDeny, nil 177 | default: 178 | return "", fmt.Errorf("invalid effect: %s", rawEffect) 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /internal/aws/iam_evaluation/policy_parser_test.go: -------------------------------------------------------------------------------- 1 | package iam_evaluation 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "os" 6 | "path/filepath" 7 | "reflect" 8 | "runtime" 9 | "testing" 10 | ) 11 | 12 | func getTestPolicyFile(name string) string { 13 | _, filename, _, _ := runtime.Caller(0) 14 | path := filepath.Join(filepath.Dir(filename), "test_policies", name+".json") 15 | // read file and return contents 16 | btes, err := os.ReadFile(path) 17 | if err != nil { 18 | panic(err) 19 | } 20 | return string(btes) 21 | } 22 | 23 | func TestPolicyParser(t *testing.T) { 24 | scenarios := []struct { 25 | Name string 26 | PolicyFile string 27 | WantErr bool 28 | WantPolicy Policy 29 | }{ 30 | { 31 | PolicyFile: "allow_assume_by_ec2", 32 | WantPolicy: Policy{ 33 | Statements: []*PolicyStatement{ 34 | { 35 | Effect: AuthorizationDecisionAllow, 36 | AllowedActions: []string{"sts:AssumeRole"}, 37 | AllowedPrincipals: []*Principal{ 38 | {Type: PrincipalTypeService, ID: "ec2.amazonaws.com"}, 39 | }, 40 | Conditions: []*Condition{}, 41 | }, 42 | }, 43 | }, 44 | }, 45 | { 46 | PolicyFile: "allow_oidc_with_condition", 47 | WantPolicy: Policy{ 48 | Statements: []*PolicyStatement{ 49 | { 50 | Effect: AuthorizationDecisionAllow, 51 | AllowedActions: []string{"sts:AssumeRoleWithWebIdentity"}, 52 | AllowedPrincipals: []*Principal{ 53 | {Type: PrincipalTypeFederated, ID: "arn:aws:iam::11112222333:oidc-provider/auth.example.com"}, 54 | }, 55 | Conditions: []*Condition{ 56 | {Key: "auth.example.com:sub", Operator: "StringEquals", AllowedValues: []string{"Administrator"}}, 57 | {Key: "auth.example.com:aud", Operator: "StringEquals", AllowedValues: []string{"MyappWebIdentity"}}, 58 | }, 59 | }, 60 | }, 61 | }, 62 | }, 63 | { 64 | PolicyFile: "eks_irsa", 65 | WantPolicy: Policy{ 66 | Statements: []*PolicyStatement{ 67 | { 68 | Effect: AuthorizationDecisionAllow, 69 | AllowedActions: []string{"sts:AssumeRoleWithWebIdentity"}, 70 | AllowedPrincipals: []*Principal{ 71 | {Type: PrincipalTypeFederated, ID: "arn:aws:iam::111122223333:oidc-provider/oidc.eks.region-code.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE"}, 72 | }, 73 | Conditions: []*Condition{ 74 | {Key: "oidc.eks.region-code.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE:sub", Operator: "StringEquals", AllowedValues: []string{"system:serviceaccount:default:my-service-account"}}, 75 | {Key: "oidc.eks.region-code.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE:aud", Operator: "StringEquals", AllowedValues: []string{"sts.amazonaws.com"}}, 76 | }, 77 | }, 78 | }, 79 | }, 80 | }, 81 | { 82 | PolicyFile: "eks_irsa_stringlike", 83 | WantPolicy: Policy{ 84 | Statements: []*PolicyStatement{ 85 | { 86 | Effect: AuthorizationDecisionAllow, 87 | AllowedActions: []string{"sts:AssumeRoleWithWebIdentity"}, 88 | AllowedPrincipals: []*Principal{ 89 | {Type: PrincipalTypeFederated, ID: "arn:aws:iam::111122223333:oidc-provider/oidc.eks.region-code.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE"}, 90 | }, 91 | Conditions: []*Condition{ 92 | {Key: "oidc.eks.region-code.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE:sub", Operator: "StringLike", AllowedValues: []string{"system:serviceaccount:my-ns1:*", "system:serviceaccount:my-ns2:*"}}, 93 | {Key: "oidc.eks.region-code.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE:aud", Operator: "StringEquals", AllowedValues: []string{"sts.amazonaws.com"}}, 94 | }, 95 | }, 96 | }, 97 | }, 98 | }, 99 | } 100 | 101 | for _, scenario := range scenarios { 102 | t.Run(scenario.PolicyFile, func(t *testing.T) { 103 | policy, err := ParseRoleTrustPolicy(getTestPolicyFile(scenario.PolicyFile)) 104 | if (err != nil) != scenario.WantErr { 105 | t.Errorf("expected error: %v, got: %v", scenario.WantErr, err) 106 | } 107 | assert.Len(t, policy.Statements, len(scenario.WantPolicy.Statements)) 108 | for i, wantStatement := range scenario.WantPolicy.Statements { 109 | gotStatement := policy.Statements[i] 110 | assert.Equal(t, wantStatement.Effect, gotStatement.Effect, "effect statement %d", i) 111 | assert.ElementsMatchf(t, wantStatement.AllowedActions, gotStatement.AllowedActions, "actions statement %d", i) 112 | assert.ElementsMatchf(t, wantStatement.Conditions, gotStatement.Conditions, "condition statement %d", i) 113 | assert.ElementsMatchf(t, wantStatement.AllowedPrincipals, gotStatement.AllowedPrincipals, "principal statement %d", i) 114 | } 115 | }) 116 | } 117 | } 118 | 119 | func Test_ensureStringArray(t *testing.T) { 120 | tests := []struct { 121 | name string 122 | action interface{} 123 | want []string 124 | wantErr bool 125 | }{ 126 | {name: "parse string action", action: "foo", want: []string{"foo"}}, 127 | {name: "parse string array action", action: []string{"foo", "bar"}, want: []string{"foo", "bar"}}, 128 | {name: "parse string array actions as interface", action: []interface{}{"foo", "bar"}, want: []string{"foo", "bar"}}, 129 | {name: "parse invalid action", action: 42, wantErr: true}, 130 | } 131 | for _, tt := range tests { 132 | t.Run(tt.name, func(t *testing.T) { 133 | got, err := ensureStringArray(tt.action) 134 | if (err != nil) != tt.wantErr { 135 | t.Errorf("ensureStringArray() error = %v, wantErr %v", err, tt.wantErr) 136 | return 137 | } 138 | if !reflect.DeepEqual(got, tt.want) { 139 | t.Errorf("ensureStringArray() got = %v, want %v", got, tt.want) 140 | } 141 | }) 142 | } 143 | } 144 | 145 | func Test_parseSinglePrincipal(t *testing.T) { 146 | type args struct { 147 | rawPrincipalType string 148 | principalID interface{} 149 | } 150 | tests := []struct { 151 | name string 152 | args args 153 | want []*Principal 154 | wantErr bool 155 | }{ 156 | { 157 | name: "parse string principal", 158 | args: args{ 159 | rawPrincipalType: "aws", 160 | principalID: "foo", 161 | }, 162 | want: []*Principal{{Type: PrincipalTypeAWS, ID: "foo"}}, 163 | }, 164 | { 165 | name: "parse array principal", 166 | args: args{ 167 | rawPrincipalType: "federated", 168 | principalID: []string{"foo", "bar"}, 169 | }, 170 | want: []*Principal{ 171 | {Type: PrincipalTypeFederated, ID: "foo"}, 172 | {Type: PrincipalTypeFederated, ID: "bar"}, 173 | }, 174 | }, 175 | { 176 | name: "parse invalid principal", 177 | args: args{ 178 | rawPrincipalType: "IDoNotExist", 179 | principalID: "foo", 180 | }, 181 | wantErr: true, 182 | }, 183 | } 184 | for _, tt := range tests { 185 | t.Run(tt.name, func(t *testing.T) { 186 | got, err := parseSinglePrincipal(tt.args.rawPrincipalType, tt.args.principalID) 187 | if (err != nil) != tt.wantErr { 188 | t.Errorf("parseSinglePrincipal() error = %v, wantErr %v", err, tt.wantErr) 189 | return 190 | } 191 | if !reflect.DeepEqual(got, tt.want) { 192 | t.Errorf("parseSinglePrincipal() got = %v, want %v", got, tt.want) 193 | } 194 | }) 195 | } 196 | } 197 | 198 | func Test_parseConditions(t *testing.T) { 199 | tests := []struct { 200 | name string 201 | rawConditions map[string]map[string]interface{} 202 | want []*Condition 203 | wantErr bool 204 | }{ 205 | { 206 | rawConditions: map[string]map[string]interface{}{"StringEquals": {"foo": "bar"}}, 207 | want: []*Condition{{Operator: "StringEquals", Key: "foo", AllowedValues: []string{"bar"}}}, 208 | }, 209 | { 210 | rawConditions: map[string]map[string]interface{}{"StringEquals": {"foo": []string{"bar", "baz"}}}, 211 | want: []*Condition{{Operator: "StringEquals", Key: "foo", AllowedValues: []string{"bar", "baz"}}}, 212 | }, 213 | { 214 | rawConditions: map[string]map[string]interface{}{"StringEquals": {"foo": "bar", "fooz": "baz"}}, 215 | want: []*Condition{ 216 | {Operator: "StringEquals", Key: "foo", AllowedValues: []string{"bar"}}, 217 | {Operator: "StringEquals", Key: "fooz", AllowedValues: []string{"baz"}}, 218 | }, 219 | }, 220 | } 221 | 222 | for _, tt := range tests { 223 | t.Run(tt.name, func(t *testing.T) { 224 | got, err := parseConditions(tt.rawConditions) 225 | if (err != nil) != tt.wantErr { 226 | t.Errorf("parseConditions() error = %v, wantErr %v", err, tt.wantErr) 227 | return 228 | } 229 | if !reflect.DeepEqual(got, tt.want) { 230 | t.Errorf("parseConditions() got = %v, want %v", got, tt.want) 231 | } 232 | }) 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /internal/aws/iam_evaluation/policy_test.go: -------------------------------------------------------------------------------- 1 | package iam_evaluation 2 | 3 | import "testing" 4 | 5 | func allowPolicyStatementThatNeverMatches() *PolicyStatement { 6 | return &PolicyStatement{ 7 | Effect: AuthorizationDecisionAllow, 8 | AllowedPrincipals: []*Principal{}, 9 | AllowedActions: []string{}, 10 | Conditions: []*Condition{}, 11 | } 12 | } 13 | 14 | func allowPolicyStatementThatAlwaysMatches() *PolicyStatement { 15 | return &PolicyStatement{ 16 | Effect: AuthorizationDecisionAllow, 17 | AllowedPrincipals: []*Principal{{Type: PrincipalTypeUnknown, ID: "*"}}, 18 | AllowedActions: []string{"*"}, 19 | Conditions: []*Condition{}, 20 | } 21 | } 22 | 23 | func explicitDenyThatAlwaysMatches() *PolicyStatement { 24 | return &PolicyStatement{ 25 | Effect: AuthorizationDecisionDeny, 26 | AllowedPrincipals: []*Principal{{Type: PrincipalTypeUnknown, ID: "*"}}, 27 | AllowedActions: []string{"*"}, 28 | Conditions: []*Condition{}, 29 | } 30 | } 31 | 32 | func TestPolicy(t *testing.T) { 33 | scenarios := []struct { 34 | Name string 35 | Policy Policy 36 | Expect AuthorizationResult 37 | }{ 38 | { 39 | Name: "a policy with no statement should deny", 40 | Policy: Policy{Statements: []*PolicyStatement{}}, 41 | Expect: AuthorizationResultDeny, 42 | }, 43 | { 44 | Name: "a policy with no matching statement should deny", 45 | Policy: Policy{Statements: []*PolicyStatement{allowPolicyStatementThatNeverMatches(), allowPolicyStatementThatNeverMatches()}}, 46 | Expect: AuthorizationResultDeny, 47 | }, 48 | { 49 | Name: "a policy with 1 matching statement should allow", 50 | Policy: Policy{Statements: []*PolicyStatement{allowPolicyStatementThatAlwaysMatches()}}, 51 | Expect: AuthorizationResultAllow, 52 | }, 53 | { 54 | Name: "a policy with 1 matching statement and 1 non matching statement should allow", 55 | Policy: Policy{Statements: []*PolicyStatement{allowPolicyStatementThatAlwaysMatches(), allowPolicyStatementThatNeverMatches()}}, 56 | Expect: AuthorizationResultAllow, 57 | }, 58 | { 59 | Name: "a policy with 1 explicit deny statement should deny", 60 | Policy: Policy{Statements: []*PolicyStatement{explicitDenyThatAlwaysMatches()}}, 61 | Expect: AuthorizationResultDeny, 62 | }, 63 | { 64 | Name: "a policy with 1 matching allow statement and 1 explicit deny matching statement should deny", 65 | Policy: Policy{Statements: []*PolicyStatement{allowPolicyStatementThatAlwaysMatches(), explicitDenyThatAlwaysMatches()}}, 66 | Expect: AuthorizationResultDeny, 67 | }, 68 | } 69 | 70 | for _, scenario := range scenarios { 71 | t.Run(scenario.Name, func(t *testing.T) { 72 | result := *scenario.Policy.Authorize(&AuthorizationContext{Principal: &Principal{PrincipalTypeUnknown, "foo"}}) 73 | if result != scenario.Expect { 74 | t.Errorf("Expected %v, got %v", scenario.Expect, result) 75 | } 76 | }) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /internal/aws/iam_evaluation/statement.go: -------------------------------------------------------------------------------- 1 | package iam_evaluation 2 | 3 | import ( 4 | "path/filepath" 5 | "strings" 6 | ) 7 | 8 | type PrincipalType string 9 | 10 | const ( 11 | PrincipalTypeUnknown = PrincipalType("") 12 | PrincipalTypeAny = "[any]" 13 | PrincipalTypeAWS = PrincipalType("AWS") 14 | PrincipalTypeService = PrincipalType("Service") 15 | PrincipalTypeFederated = PrincipalType("Federated") 16 | PrincipalTypeCanonicalUser = PrincipalType("CanonicalUser") 17 | ) 18 | 19 | type Principal struct { 20 | Type PrincipalType 21 | ID string 22 | } 23 | 24 | type PolicyStatement struct { 25 | Effect AuthorizationDecision 26 | AllowedPrincipals []*Principal 27 | AllowedActions []string 28 | Conditions []*Condition 29 | } 30 | 31 | func (m *PolicyStatement) Authorize(context *AuthorizationContext) *AuthorizationResult { 32 | statementMatches := m.statementMatches(context) 33 | if !statementMatches { 34 | // The statement does not match, no authorization decision 35 | return &AuthorizationResultNoDecision 36 | } 37 | 38 | if m.Effect == AuthorizationDecisionAllow { 39 | return &AuthorizationResultAllow // Explicit allow 40 | } 41 | return &AuthorizationResultDeny // Explicit deny 42 | } 43 | 44 | func (m *PolicyStatement) statementMatches(context *AuthorizationContext) bool { 45 | return m.actionMatches(context.Action) && 46 | m.principalMatches(context.Principal) && 47 | m.conditionsMatch(context) 48 | } 49 | 50 | func (m *PolicyStatement) conditionsMatch(context *AuthorizationContext) bool { 51 | if len(m.Conditions) == 0 { 52 | return true // no conditions 53 | } 54 | 55 | // Conditions are AND'ed together 56 | for _, condition := range m.Conditions { 57 | if !condition.Matches(context) { 58 | // At least one condition doesn't match, deny authorization 59 | return false 60 | } 61 | } 62 | 63 | // We verified that all conditions matched 64 | return true 65 | } 66 | 67 | func (m *PolicyStatement) actionMatches(action string) bool { 68 | action = strings.ToLower(action) 69 | for _, allowedAction := range m.AllowedActions { 70 | if match, err := filepath.Match(strings.ToLower(allowedAction), action); match && err == nil { 71 | return true 72 | } 73 | } 74 | return false 75 | } 76 | 77 | func (m *PolicyStatement) principalMatches(principal *Principal) bool { 78 | for _, allowedPrincipal := range m.AllowedPrincipals { 79 | if allowedPrincipal.Type == PrincipalTypeAny { 80 | return true 81 | } 82 | 83 | if allowedPrincipal.Type != principal.Type { 84 | continue 85 | } 86 | 87 | if match, err := filepath.Match(allowedPrincipal.ID, principal.ID); match && err == nil { 88 | return true 89 | } 90 | } 91 | return false 92 | } 93 | -------------------------------------------------------------------------------- /internal/aws/iam_evaluation/statement_test.go: -------------------------------------------------------------------------------- 1 | package iam_evaluation 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestPolicyStatement_Authorize(t *testing.T) { 8 | 9 | tests := []struct { 10 | Name string 11 | Statement PolicyStatement 12 | Context AuthorizationContext 13 | Expect AuthorizationResult 14 | }{ 15 | { 16 | Name: "Simple statement matching should allow", 17 | Statement: PolicyStatement{ 18 | Effect: AuthorizationDecisionAllow, 19 | AllowedActions: []string{"ec2:CreateInstance"}, 20 | AllowedPrincipals: []*Principal{{Type: PrincipalTypeAny}}, 21 | }, 22 | Context: AuthorizationContext{ 23 | Action: "ec2:CreateInstance", 24 | Principal: &Principal{Type: PrincipalTypeAWS, ID: "foo"}, 25 | ContextKeys: map[string]string{}, 26 | }, 27 | Expect: AuthorizationResultAllow, 28 | }, 29 | { 30 | Name: "Simple statement not matching should not allow", 31 | Statement: PolicyStatement{ 32 | Effect: AuthorizationDecisionAllow, 33 | AllowedActions: []string{"ec2:CreateInstance"}, 34 | AllowedPrincipals: []*Principal{{Type: PrincipalTypeAny}}, 35 | }, 36 | Context: AuthorizationContext{ 37 | Action: "ec2:SomethingElse", 38 | Principal: &Principal{Type: PrincipalTypeAWS, ID: "foo"}, 39 | ContextKeys: map[string]string{}, 40 | }, 41 | Expect: AuthorizationResultNoDecision, 42 | }, 43 | { 44 | Name: "Simple statement not matching principal should not allow", 45 | Statement: PolicyStatement{ 46 | Effect: AuthorizationDecisionAllow, 47 | AllowedActions: []string{"ec2:CreateInstance"}, 48 | AllowedPrincipals: []*Principal{{Type: PrincipalTypeAWS, ID: "foobar"}}, 49 | }, 50 | Context: AuthorizationContext{ 51 | Action: "ec2:CreateInstance", 52 | Principal: &Principal{Type: PrincipalTypeAWS, ID: "foo"}, 53 | ContextKeys: map[string]string{}, 54 | }, 55 | Expect: AuthorizationResultNoDecision, 56 | }, 57 | { 58 | Name: "Explicit deny statement matching should deny", 59 | Statement: PolicyStatement{ 60 | Effect: AuthorizationDecisionDeny, 61 | AllowedActions: []string{"ec2:CreateInstance"}, 62 | AllowedPrincipals: []*Principal{{Type: PrincipalTypeAny}}, 63 | }, 64 | Context: AuthorizationContext{ 65 | Action: "ec2:CreateInstance", 66 | Principal: &Principal{Type: PrincipalTypeAWS, ID: "foo"}, 67 | ContextKeys: map[string]string{}, 68 | }, 69 | Expect: AuthorizationResultDeny, 70 | }, 71 | { 72 | Name: "Explicit deny statement not matching should not deny", 73 | Statement: PolicyStatement{ 74 | Effect: AuthorizationDecisionDeny, 75 | AllowedActions: []string{"ec2:CreateInstance"}, 76 | AllowedPrincipals: []*Principal{{Type: PrincipalTypeAny}}, 77 | }, 78 | Context: AuthorizationContext{ 79 | Action: "ec2:SomethingElse", 80 | Principal: &Principal{Type: PrincipalTypeAWS, ID: "foo"}, 81 | ContextKeys: map[string]string{}, 82 | }, 83 | Expect: AuthorizationResultNoDecision, 84 | }, 85 | { 86 | Name: "Statement with simple condition should allow", 87 | Statement: PolicyStatement{ 88 | Effect: AuthorizationDecisionAllow, 89 | AllowedActions: []string{"ec2:CreateInstance"}, 90 | AllowedPrincipals: []*Principal{{Type: PrincipalTypeAny}}, 91 | Conditions: []*Condition{ 92 | {Key: "aws:MyKey", Operator: "StringEquals", AllowedValues: []string{"foo"}}, 93 | }, 94 | }, 95 | Context: AuthorizationContext{ 96 | Action: "ec2:CreateInstance", 97 | Principal: &Principal{Type: PrincipalTypeAWS, ID: "foo"}, 98 | ContextKeys: map[string]string{ 99 | "aws:MyKey": "foo", 100 | }, 101 | }, 102 | Expect: AuthorizationResultAllow, 103 | }, 104 | { 105 | Name: "Statement with condition should allow - allowed values should be OR'ed together", 106 | Statement: PolicyStatement{ 107 | Effect: AuthorizationDecisionAllow, 108 | AllowedActions: []string{"ec2:CreateInstance"}, 109 | AllowedPrincipals: []*Principal{{Type: PrincipalTypeAny}}, 110 | Conditions: []*Condition{ 111 | {Key: "aws:MyKey", Operator: "StringEquals", AllowedValues: []string{"foo", "bar"}}, 112 | }, 113 | }, 114 | Context: AuthorizationContext{ 115 | Action: "ec2:CreateInstance", 116 | Principal: &Principal{Type: PrincipalTypeAWS, ID: "foo"}, 117 | ContextKeys: map[string]string{ 118 | "aws:MyKey": "bar", 119 | }, 120 | }, 121 | Expect: AuthorizationResultAllow, 122 | }, 123 | { 124 | Name: "Statement with multiple conditions should allow - individual conditions should be AND'ed together", 125 | Statement: PolicyStatement{ 126 | Effect: AuthorizationDecisionAllow, 127 | AllowedActions: []string{"ec2:CreateInstance"}, 128 | AllowedPrincipals: []*Principal{{Type: PrincipalTypeAny}}, 129 | Conditions: []*Condition{ 130 | {Key: "aws:MyKey", Operator: "StringEquals", AllowedValues: []string{"foo"}}, 131 | {Key: "aws:MyOtherKey", Operator: "StringEquals", AllowedValues: []string{"bar"}}, 132 | }, 133 | }, 134 | Context: AuthorizationContext{ 135 | Action: "ec2:CreateInstance", 136 | Principal: &Principal{Type: PrincipalTypeAWS, ID: "foo"}, 137 | ContextKeys: map[string]string{ 138 | "aws:MyKey": "foo", 139 | "aws:MyOtherKey": "bar", 140 | }, 141 | }, 142 | Expect: AuthorizationResultAllow, 143 | }, 144 | { 145 | Name: "Statement with multiple conditions should allow - individual conditions should be AND'ed together and each value within OR'ed", 146 | Statement: PolicyStatement{ 147 | Effect: AuthorizationDecisionAllow, 148 | AllowedActions: []string{"ec2:CreateInstance"}, 149 | AllowedPrincipals: []*Principal{{Type: PrincipalTypeAny}}, 150 | Conditions: []*Condition{ 151 | {Key: "aws:MyKey", Operator: "StringEquals", AllowedValues: []string{"foo", "fooz"}}, 152 | {Key: "aws:MyOtherKey", Operator: "StringEquals", AllowedValues: []string{"bar", "baz"}}, 153 | }, 154 | }, 155 | Context: AuthorizationContext{ 156 | Action: "ec2:CreateInstance", 157 | Principal: &Principal{Type: PrincipalTypeAWS, ID: "foo"}, 158 | ContextKeys: map[string]string{ 159 | "aws:MyKey": "fooz", 160 | "aws:MyOtherKey": "bar", 161 | }, 162 | }, 163 | Expect: AuthorizationResultAllow, 164 | }, 165 | { 166 | Name: "StringLike condition", 167 | Statement: PolicyStatement{ 168 | Effect: AuthorizationDecisionAllow, 169 | AllowedActions: []string{"ec2:CreateInstance"}, 170 | AllowedPrincipals: []*Principal{{Type: PrincipalTypeAny}}, 171 | Conditions: []*Condition{ 172 | {Key: "aws:InstanceType", Operator: "StringLike", AllowedValues: []string{"t2.*"}}, 173 | }, 174 | }, 175 | Context: AuthorizationContext{ 176 | Action: "ec2:CreateInstance", 177 | Principal: &Principal{Type: PrincipalTypeAWS, ID: "foo"}, 178 | ContextKeys: map[string]string{ 179 | "aws:InstanceType": "t2.medium", 180 | }, 181 | }, 182 | Expect: AuthorizationResultAllow, 183 | }, 184 | { 185 | Name: "StringLike condition negative case", 186 | Statement: PolicyStatement{ 187 | Effect: AuthorizationDecisionAllow, 188 | AllowedActions: []string{"ec2:CreateInstance"}, 189 | AllowedPrincipals: []*Principal{{Type: PrincipalTypeAny}}, 190 | Conditions: []*Condition{ 191 | {Key: "aws:InstanceType", Operator: "StringLike", AllowedValues: []string{"t2.*"}}, 192 | }, 193 | }, 194 | Context: AuthorizationContext{ 195 | Action: "ec2:CreateInstance", 196 | Principal: &Principal{Type: PrincipalTypeAWS, ID: "foo"}, 197 | ContextKeys: map[string]string{ 198 | "aws:InstanceType": "m3.2xlarge", 199 | }, 200 | }, 201 | Expect: AuthorizationResultNoDecision, 202 | }, 203 | { 204 | Name: "Action matching should be case-insensitive", 205 | Statement: PolicyStatement{ 206 | Effect: AuthorizationDecisionAllow, 207 | AllowedPrincipals: []*Principal{{Type: PrincipalTypeAny}}, 208 | AllowedActions: []string{"EC2:CreateInstance"}, 209 | Conditions: []*Condition{}, 210 | }, 211 | Context: AuthorizationContext{ 212 | Action: "ec2:CreateInstance", 213 | Principal: &Principal{Type: PrincipalTypeAWS, ID: "foo"}, 214 | ContextKeys: map[string]string{}, 215 | }, 216 | Expect: AuthorizationResultAllow, 217 | }, 218 | } 219 | for _, tt := range tests { 220 | t.Run(tt.Name, func(t *testing.T) { 221 | result := *tt.Statement.Authorize(&tt.Context) 222 | if result != tt.Expect { 223 | t.Errorf("Expected %v, got %v", tt.Expect, result) 224 | } 225 | }) 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /internal/aws/iam_evaluation/test_policies/allow_assume_by_ec2.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": "sts:AssumeRole", 7 | "Principal": { 8 | "Service": "ec2.amazonaws.com" 9 | } 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /internal/aws/iam_evaluation/test_policies/allow_oidc_with_condition.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Principal": { 7 | "Federated": "arn:aws:iam::11112222333:oidc-provider/auth.example.com" 8 | }, 9 | "Action": "sts:AssumeRoleWithWebIdentity", 10 | "Condition": { 11 | "StringEquals": { 12 | "auth.example.com:sub": "Administrator", 13 | "auth.example.com:aud": "MyappWebIdentity" 14 | } 15 | } 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /internal/aws/iam_evaluation/test_policies/eks_irsa.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Principal": { 7 | "Federated": "arn:aws:iam::111122223333:oidc-provider/oidc.eks.region-code.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE" 8 | }, 9 | "Action": "sts:AssumeRoleWithWebIdentity", 10 | "Condition": { 11 | "StringEquals": { 12 | "oidc.eks.region-code.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE:sub": "system:serviceaccount:default:my-service-account", 13 | "oidc.eks.region-code.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE:aud": "sts.amazonaws.com" 14 | } 15 | } 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /internal/aws/iam_evaluation/test_policies/eks_irsa_stringlike.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Principal": { 7 | "Federated": "arn:aws:iam::111122223333:oidc-provider/oidc.eks.region-code.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE" 8 | }, 9 | "Action": "sts:AssumeRoleWithWebIdentity", 10 | "Condition": { 11 | "StringEquals": { 12 | "oidc.eks.region-code.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE:aud": "sts.amazonaws.com" 13 | }, 14 | "StringLike": { 15 | "oidc.eks.region-code.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE:sub": [ 16 | "system:serviceaccount:my-ns1:*", 17 | "system:serviceaccount:my-ns2:*" 18 | ] 19 | } 20 | } 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /internal/utils/aws.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | "github.com/aws/aws-sdk-go-v2/aws" 8 | "github.com/aws/aws-sdk-go-v2/config" 9 | ) 10 | 11 | func AWSClient() *aws.Config { 12 | cfg, err := config.LoadDefaultConfig(context.Background()) 13 | if err != nil { 14 | log.Fatalf("unable to load AWS configuration, %v", err) 15 | } 16 | return &cfg 17 | } 18 | -------------------------------------------------------------------------------- /internal/utils/case_insensitive_map.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "strings" 4 | 5 | type CaseInsensitiveMap[T any] struct { 6 | entries map[string]T 7 | } 8 | 9 | func NewCaseInsensitiveMap[T any](from *map[string]T) *CaseInsensitiveMap[T] { 10 | ciMap := &CaseInsensitiveMap[T]{} 11 | ciMap.initFrom(from) 12 | return ciMap 13 | } 14 | 15 | func (m *CaseInsensitiveMap[T]) Get(key string) (T, bool) { 16 | value, ok := m.entries[strings.ToLower(key)] 17 | return value, ok 18 | } 19 | 20 | func (m *CaseInsensitiveMap[T]) initFrom(entries *map[string]T) { 21 | m.entries = make(map[string]T) 22 | for key, value := range *entries { 23 | m.entries[strings.ToLower(key)] = value 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /internal/utils/file.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "os" 4 | 5 | func FileExists(path string) bool { 6 | _, err := os.Stat(path) 7 | if os.IsNotExist(err) { 8 | return false 9 | } else if err != nil { 10 | // In case of error, we assume the file doesn't exist to make the logic simpler 11 | return false 12 | } 13 | return true 14 | } 15 | -------------------------------------------------------------------------------- /internal/utils/kubernetes.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "log" 5 | "net/url" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "k8s.io/client-go/kubernetes" 11 | "k8s.io/client-go/rest" 12 | "k8s.io/client-go/tools/clientcmd" 13 | "k8s.io/client-go/util/homedir" 14 | ) 15 | 16 | func getKubeConfigPath() string { 17 | // if KUBECONFIG is set, use it 18 | if kubeConfigEnvPath := os.Getenv("KUBECONFIG"); kubeConfigEnvPath != "" { 19 | return kubeConfigEnvPath 20 | } 21 | 22 | // Otherwise, use $HOME/.kube/config if it exists 23 | if kubeConfigFilePath := filepath.Join(homedir.HomeDir(), ".kube/config"); FileExists(kubeConfigFilePath) { 24 | return kubeConfigFilePath 25 | } 26 | 27 | // Otherwise, return an empty string 28 | // This will cause `clientcmd.BuildConfigFromFlags` called in `GetClient` will try to use 29 | // in-cluster auth 30 | // c.f. https://pkg.go.dev/k8s.io/client-go/tools/clientcmd#BuildConfigFromFlags 31 | return "" 32 | } 33 | 34 | func getConfig() *rest.Config { 35 | config, err := clientcmd.BuildConfigFromFlags("", getKubeConfigPath()) 36 | if err != nil { 37 | log.Fatalf("unable to build kube config: %v", err) 38 | } 39 | return config 40 | } 41 | 42 | func K8sClient() *kubernetes.Clientset { 43 | k8sClient, err := kubernetes.NewForConfig(getConfig()) 44 | if err != nil { 45 | log.Fatalf("unable to create kube client: %v", err) 46 | } 47 | return k8sClient 48 | } 49 | 50 | // IsEKS determines if the cluster in the current context does appear to be an EKS cluster 51 | func IsEKS() bool { 52 | parsedUrl, err := url.Parse(getConfig().Host) 53 | if err != nil { 54 | return false 55 | } 56 | return strings.HasSuffix(parsedUrl.Host, ".eks.amazonaws.com") 57 | } 58 | 59 | func GetEKSClusterName() string { 60 | // Most of (if not all) the time, the KubeConfig file generated by "aws eks update-kubeconfig" will have an 61 | // ExecProvider section that runs "aws eks get-token <...> --cluster-name foo" 62 | // We parse it and extract the cluster name from there 63 | 64 | execProvider := getConfig().ExecProvider 65 | if execProvider == nil || execProvider.Command != "aws" { 66 | return "" 67 | } 68 | for i, arg := range execProvider.Args { 69 | if arg == "--cluster-name" && i+1 < len(execProvider.Args) { 70 | return execProvider.Args[i+1] 71 | } 72 | } 73 | return "" 74 | } 75 | -------------------------------------------------------------------------------- /permissions.md: -------------------------------------------------------------------------------- 1 | # Permissions needed to run MKAT 2 | 3 | To be able to run MKAT and benefit from all its features, you need the following permissions. 4 | 5 | ## AWS permissions 6 | 7 | ```json 8 | { 9 | "Version": "2012-10-17", 10 | "Statement": [ 11 | { 12 | "Effect": "Allow", 13 | "Action": [ 14 | "eks:DescribeCluster", 15 | "iam:ListRoles" 16 | ], 17 | "Resource": "*" 18 | } 19 | ] 20 | } 21 | ``` 22 | 23 | Optionally, you can restrict `eks:DescribeCluster` to the specific EKS cluster you want to analyze, e.g. 24 | 25 | ```json 26 | { 27 | "Version": "2012-10-17", 28 | "Statement": [ 29 | { 30 | "Effect": "Allow", 31 | "Action": [ 32 | "eks:DescribeCluster" 33 | ], 34 | "Resource": "arn:aws:eks:us-east-1:012345678901:cluster/your-eks-cluster" 35 | }, 36 | { 37 | "Effect": "Allow", 38 | "Action": [ 39 | "iam:ListRoles" 40 | ], 41 | "Resource": "*" 42 | } 43 | ] 44 | } 45 | ``` 46 | 47 | ## Kubernetes permissions 48 | 49 | You will need a `ClusterRole` with the following permissions: 50 | 51 | ```yaml 52 | apiVersion: rbac.authorization.k8s.io/v1 53 | kind: ClusterRole 54 | metadata: 55 | name: mkat 56 | rules: 57 | # mkat eks find-role-relationships 58 | - apiGroups: [""] 59 | resources: ["serviceaccounts", "pods"] 60 | verbs: ["list"] 61 | # mkat eks find-secrets 62 | - apiGroups: [""] 63 | resources: ["pods", "secrets", "configmaps"] 64 | verbs: ["list"] 65 | # mkat eks test-imds 66 | - apiGroups: [""] 67 | resources: ["pods"] 68 | verbs: ["list", "get", "create", "delete"] 69 | - apiGroups: [""] 70 | resources: ["pods/log"] 71 | verbs: ["get"] 72 | ``` 73 | 74 | In EKS, you can for instance bind this ClusterRole to a `mkat-users` group, then use the [`aws-auth`](https://securitylabs.datadoghq.com/articles/amazon-eks-attacking-securing-cloud-identities/#authorization-the-aws-auth-configmap) ConfigMap to assign the group to your AWS identity: 75 | 76 | ```bash 77 | kubectl create clusterrolebinding mkat --clusterrole=mkat --group=mkat-users 78 | ``` 79 | 80 | ```yaml 81 | apiVersion: v1 82 | kind: ConfigMap 83 | metadata: 84 | name: aws-auth 85 | namespace: kube-system 86 | data: 87 | mapRoles: | 88 | # ... 89 | - rolearn: arn:aws:iam::012345678901:role/your-role 90 | groups: ["mkat-users"] 91 | username: mkat-user:{{SessionName}} 92 | mapUsers: | 93 | [] 94 | ``` -------------------------------------------------------------------------------- /pkg/managed-kubernetes-auditing-toolkit/eks/imds/imds_tester.go: -------------------------------------------------------------------------------- 1 | package imds 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | "os" 9 | "os/signal" 10 | "strings" 11 | "syscall" 12 | "time" 13 | 14 | "k8s.io/apimachinery/pkg/api/resource" 15 | v1 "k8s.io/api/core/v1" 16 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 17 | "k8s.io/apimachinery/pkg/util/wait" 18 | "k8s.io/client-go/kubernetes" 19 | typedv1 "k8s.io/client-go/kubernetes/typed/core/v1" 20 | ) 21 | 22 | type ImdsTester struct { 23 | K8sClient *kubernetes.Clientset 24 | Namespace string 25 | } 26 | 27 | type ImdsTestResult struct { 28 | IsImdsAccessible bool 29 | ResultDescription string 30 | } 31 | 32 | const ImdsTesterV1PodName = "mkat-imds-tester" 33 | const ImdsTesterV2PodName = "mkat-imds-v2-tester" 34 | 35 | func (m *ImdsTester) TestImdsV1Accessible() (*ImdsTestResult, error) { 36 | commandToRun := []string{ 37 | "sh", 38 | "-c", 39 | "(curl --silent --show-error --connect-timeout 2 169.254.169.254/latest/meta-data/iam/security-credentials/ || true)", 40 | } 41 | podLogs, err := m.runCommandInPodAndGetLogs(ImdsTesterV1PodName, commandToRun) 42 | if err != nil { 43 | return nil, fmt.Errorf("unable to retrieve logs from IMDS tester pod: %v", err) 44 | } 45 | 46 | // Case 1: no network connection (e.g. NetworkPolicy in place) 47 | if strings.Contains(podLogs, "Failed to connect") { 48 | return &ImdsTestResult{ 49 | IsImdsAccessible: false, 50 | ResultDescription: "unable to establish a network connection to the IMDS", 51 | }, nil 52 | } 53 | 54 | // Case 2: IMDSv2 enforced, IMDSv1 is accessible at the network level but returns a 401 error 55 | if strings.TrimSpace(podLogs) == "" { 56 | return &ImdsTestResult{ 57 | IsImdsAccessible: false, 58 | ResultDescription: "able to establish a network connection to the IMDS, but no credentials were returned", 59 | }, nil 60 | } 61 | 62 | // Case 3: IMDSv1 is accessible and returns credentials 63 | return &ImdsTestResult{ 64 | IsImdsAccessible: true, 65 | ResultDescription: fmt.Sprintf("any pod can retrieve credentials for the AWS role %s", podLogs), 66 | }, nil 67 | } 68 | 69 | func (m *ImdsTester) TestImdsV2Accessible() (*ImdsTestResult, error) { 70 | commandToRun := []string{ 71 | "sh", 72 | "-c", 73 | // We use "--max-time" because when the IMDS max-response-hop is set to 1, the TCP connection succeeds initially but hangs indefinitely when calling /latest/api/token 74 | `TOKEN=$(curl --show-error --max-time 2 --silent -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") 75 | (curl --silent --show-error --max-time 2 -H "X-aws-ec2-metadata-token: $TOKEN" 169.254.169.254/latest/meta-data/iam/security-credentials/ || true)`, 76 | } 77 | podLogs, err := m.runCommandInPodAndGetLogs(ImdsTesterV2PodName, commandToRun) 78 | if err != nil { 79 | return nil, fmt.Errorf("unable to retrieve logs from IMDS tester pod: %v", err) 80 | } 81 | 82 | // Case 1: no network connection (e.g. NetworkPolicy in place) 83 | if strings.Contains(podLogs, "Failed to connect") || strings.Contains(podLogs, "timed out") { 84 | return &ImdsTestResult{ 85 | IsImdsAccessible: false, 86 | ResultDescription: "unable to establish a network connection to the IMDS", 87 | }, nil 88 | } 89 | 90 | // Case 3: IMDSv2 is accessible and returns credentials 91 | return &ImdsTestResult{ 92 | IsImdsAccessible: true, 93 | ResultDescription: fmt.Sprintf("any pod can retrieve credentials for the AWS role %s", podLogs), 94 | }, nil 95 | } 96 | 97 | func (m *ImdsTester) runCommandInPodAndGetLogs(podName string, command []string) (string, error) { 98 | podsClient := m.K8sClient.CoreV1().Pods(m.Namespace) 99 | podDefinition := &v1.Pod{ 100 | ObjectMeta: metav1.ObjectMeta{Name: podName, Namespace: m.Namespace}, 101 | Spec: v1.PodSpec{ 102 | Containers: []v1.Container{{ 103 | Name: podName, 104 | Image: "curlimages/curl:8.00.1", 105 | Command: command, 106 | Resources: v1.ResourceRequirements{ 107 | Requests: v1.ResourceList{ 108 | v1.ResourceCPU: resource.MustParse("500m"), 109 | v1.ResourceMemory: resource.MustParse("128Mi"), 110 | }, 111 | Limits: v1.ResourceList{ 112 | v1.ResourceCPU: resource.MustParse("500m"), 113 | v1.ResourceMemory: resource.MustParse("256Mi"), 114 | }, 115 | }, 116 | }}, 117 | RestartPolicy: v1.RestartPolicyNever, // don't restart the pod once the command has been executed 118 | }, 119 | } 120 | _, err := podsClient.Create(context.Background(), podDefinition, metav1.CreateOptions{}) 121 | if err != nil { 122 | return "", fmt.Errorf("unable to create IMDS tester pod: %v", err) 123 | } 124 | m.handleCtrlC() 125 | defer removePod(podsClient, podName) 126 | 127 | err = wait.PollImmediate(1*time.Second, 120*time.Second, func() (bool, error) { 128 | return podHasSuccessfullyCompleted(podsClient, podName) 129 | }) 130 | 131 | if err != nil { 132 | return "", fmt.Errorf("unable to wait for IMDS tester pod to complete: %v", err) 133 | } 134 | 135 | // Retrieve command output 136 | podLogs, err := getPodLogs(podsClient, podName) 137 | if err != nil { 138 | return "", fmt.Errorf("unable to retrieve logs from IMDS tester pod: %v", err) 139 | } 140 | 141 | return podLogs, nil 142 | } 143 | 144 | func (m *ImdsTester) handleCtrlC() { 145 | // If the user interactively cancels the test, clean up the pod 146 | c := make(chan os.Signal, 1) 147 | signal.Notify(c, os.Interrupt, syscall.SIGTERM) 148 | go func() { 149 | <-c 150 | println("Received SIGINT, cleaning up IMDS tester pods") 151 | podsClient := m.K8sClient.CoreV1().Pods(m.Namespace) 152 | removePod(podsClient, ImdsTesterV1PodName) 153 | removePod(podsClient, ImdsTesterV2PodName) 154 | os.Exit(1) 155 | }() 156 | } 157 | 158 | func podHasSuccessfullyCompleted(podsClient typedv1.PodInterface, podName string) (bool, error) { 159 | pod, err := podsClient.Get(context.Background(), podName, metav1.GetOptions{}) 160 | if err != nil { 161 | return false, err 162 | } 163 | 164 | if pod.Status.Phase == v1.PodSucceeded { 165 | return true, nil 166 | } else if pod.Status.Phase != v1.PodPending && pod.Status.Phase != v1.PodRunning { 167 | return false, fmt.Errorf("pod %s errored and is in status %s", podName, pod.Status.Phase) 168 | } 169 | 170 | return false, nil 171 | } 172 | 173 | func getPodLogs(podsClient typedv1.PodInterface, podName string) (string, error) { 174 | podLogsRequest := podsClient.GetLogs(podName, &v1.PodLogOptions{}) 175 | podLogs, err := podLogsRequest.Stream(context.Background()) 176 | if err != nil { 177 | return "", fmt.Errorf("unable to get logs for pod %s: %v", podName, err) 178 | } 179 | defer podLogs.Close() 180 | buf := new(bytes.Buffer) 181 | _, err = io.Copy(buf, podLogs) 182 | if err != nil { 183 | return "", fmt.Errorf("unable to copy logs for pod %s: %v", podName, err) 184 | } 185 | return buf.String(), nil 186 | } 187 | 188 | func removePod(podsClient typedv1.PodInterface, podName string) { 189 | var gracePeriod int64 = 0 190 | _ = podsClient.Delete(context.Background(), podName, metav1.DeleteOptions{ 191 | GracePeriodSeconds: &gracePeriod, 192 | }) 193 | } 194 | -------------------------------------------------------------------------------- /pkg/managed-kubernetes-auditing-toolkit/eks/role_relationships/roles_resolver.go: -------------------------------------------------------------------------------- 1 | package role_relationships 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/aws/aws-sdk-go-v2/aws" 7 | "github.com/aws/aws-sdk-go-v2/aws/arn" 8 | "github.com/aws/aws-sdk-go-v2/service/eks" 9 | "github.com/aws/aws-sdk-go-v2/service/iam" 10 | "github.com/datadog/managed-kubernetes-auditing-toolkit/internal/aws/iam_evaluation" 11 | "github.com/hashicorp/go-version" 12 | corev1 "k8s.io/api/core/v1" 13 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | "k8s.io/client-go/kubernetes" 15 | "log" 16 | "net/url" 17 | "strconv" 18 | "strings" 19 | ) 20 | 21 | type AssumeIAMRoleReason string 22 | 23 | const ( 24 | AssumeIAMRoleReasonIRSA = "IAM Roles for Service Accounts" 25 | AssumeIAMRoleReasonPodIdentity = "Pod Identity" 26 | ) 27 | 28 | // https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html#pod-id-cluster-versions 29 | const PodIdentityMinSupportedK8sVersion = "1.24" 30 | 31 | // AssumableIAMRole records that an IAM role can be assumed through a specific mechanism 32 | type AssumableIAMRole struct { 33 | IAMRole *IAMRole 34 | Reason AssumeIAMRoleReason 35 | } 36 | 37 | type K8sServiceAccount struct { 38 | Name string 39 | Namespace string 40 | Annotations map[string]string 41 | AssumableRoles []*AssumableIAMRole 42 | } 43 | 44 | type K8sPod struct { 45 | Name string 46 | Namespace string 47 | ServiceAccount *K8sServiceAccount 48 | HasProjectedServiceAccountToken bool 49 | } 50 | 51 | type IAMRole struct { 52 | Arn string 53 | TrustPolicy string 54 | IsPrivileged bool 55 | } 56 | 57 | type PodIdentityAssociation struct { 58 | ID string 59 | Namespace string 60 | ServiceAccountName string 61 | RoleArn string 62 | } 63 | 64 | type EKSCluster struct { 65 | AwsClient *aws.Config 66 | K8sClient *kubernetes.Clientset 67 | 68 | Name string 69 | KubernetesVersion string // e.g. "1.24" 70 | AccountID string 71 | IssuerURL string 72 | ServiceAccountsByNamespace map[string][]*K8sServiceAccount 73 | PodsByNamespace map[string][]*K8sPod 74 | IAMRoles []*IAMRole 75 | } 76 | 77 | func (m *EKSCluster) AnalyzeRoleRelationships() error { 78 | // Start by retrieving the cluster information 79 | if err := m.retrieveClusterInformation(); err != nil { 80 | return fmt.Errorf("unable to retrieve EKS cluster information: %v", err) 81 | } 82 | 83 | // Retrieve all service accounts 84 | serviceAccountsByNamespace, err := m.retrieveServiceAccountsByNamespace() 85 | if err != nil { 86 | return fmt.Errorf("unable to retrieve service accounts: %v", err) 87 | } 88 | m.ServiceAccountsByNamespace = serviceAccountsByNamespace 89 | 90 | // Then, find all pods and link them with the service accounts 91 | podsByNamespace, err := m.getPodsByNamespace() 92 | if err != nil { 93 | return fmt.Errorf("unable to retrieve pods: %v", err) 94 | } 95 | m.PodsByNamespace = podsByNamespace 96 | 97 | // Then, retrieve all IAM roles in the account 98 | iamRoles, err := m.retrieveIAMRoles() 99 | log.Printf("Found %d IAM roles in the AWS account", len(iamRoles)) 100 | if err != nil { 101 | return fmt.Errorf("unable to list IAM roles: %v", err) 102 | } 103 | m.IAMRoles = iamRoles 104 | 105 | // Finally, launch the analysis for both IRSA and Pod Identity 106 | if err := m.AnalyzeRoleRelationshipsForIRSA(); err != nil { 107 | return fmt.Errorf("unable to analyze IRSA configuration in your cluster and account: %v", err) 108 | } 109 | 110 | if err := m.AnalyzeRoleRelationshipsForPodIdentity(); err != nil { 111 | return fmt.Errorf("unable to analyze Pod Identity configuration in your cluster and account: %v", err) 112 | } 113 | 114 | return nil 115 | } 116 | 117 | func (m *EKSCluster) AnalyzeRoleRelationshipsForPodIdentity() error { 118 | log.Println("Analyzing Pod Identity configuration of your cluster") 119 | eksClient := eks.NewFromConfig(*m.AwsClient) 120 | 121 | if !m.supportsPodIdentity() { 122 | log.Println("Your cluster runs a Kubernetes version that does not support Pod Identity - skipping") 123 | log.Println("Your K8s version is " + m.KubernetesVersion + ", and Pod Identity is supported starting from " + PodIdentityMinSupportedK8sVersion) 124 | return nil 125 | } 126 | 127 | // Step 1: Retrieve all pod associations in the cluster, and keep a map by podAssociationNamespace 128 | paginator := eks.NewListPodIdentityAssociationsPaginator(eksClient, &eks.ListPodIdentityAssociationsInput{ 129 | ClusterName: &m.Name, 130 | }) 131 | namespaceToPodIdentityAssociations := map[string][]*PodIdentityAssociation{} 132 | for paginator.HasMorePages() { 133 | podIdentityAssociations, err := paginator.NextPage(context.Background()) 134 | if err != nil { 135 | return fmt.Errorf("unable to retrieve pod identity associations: %v", err) 136 | } 137 | for _, podIdentityAssociation := range podIdentityAssociations.Associations { 138 | namespace := *podIdentityAssociation.Namespace 139 | if _, ok := namespaceToPodIdentityAssociations[namespace]; !ok { 140 | namespaceToPodIdentityAssociations[namespace] = []*PodIdentityAssociation{} 141 | } 142 | 143 | //TODO: This is duplicate because AWS across calls update this value... we need to define our own type 144 | association := podIdentityAssociation 145 | namespaceToPodIdentityAssociations[namespace] = append(namespaceToPodIdentityAssociations[namespace], &PodIdentityAssociation{ 146 | ID: *association.AssociationId, 147 | Namespace: *association.Namespace, 148 | ServiceAccountName: *association.ServiceAccount, 149 | RoleArn: "", // No role ARN yet at this point, as we need a call to DescribePodIdentityAssociation 150 | }) 151 | } 152 | } 153 | 154 | // Step 2: Map assumable roles to pods 155 | for podAssociationNamespace := range namespaceToPodIdentityAssociations { 156 | log.Println("Analyzing namespace " + podAssociationNamespace + " which has " + strconv.Itoa(len(namespaceToPodIdentityAssociations[podAssociationNamespace])) + " Pod Identity associations") 157 | for _, podAssociation := range namespaceToPodIdentityAssociations[podAssociationNamespace] { 158 | 159 | // Retrieve the role attached to the pod identity association 160 | podAssociationDetails, err := eksClient.DescribePodIdentityAssociation(context.Background(), &eks.DescribePodIdentityAssociationInput{ 161 | AssociationId: &podAssociation.ID, 162 | ClusterName: &m.Name, 163 | }) 164 | if err != nil { 165 | return fmt.Errorf("unable to describe pod identity association %s: %v", podAssociation.ID, err) 166 | } 167 | pods, ok := m.PodsByNamespace[podAssociationNamespace] 168 | if !ok { 169 | // no pods in podAssociationNamespace, go to the next one 170 | continue 171 | } 172 | 173 | // cache to avoid counting multiple IAM roles for a given SA 174 | serviceAccountsHandledForPodAssociation := map[string]bool{} 175 | 176 | // All pods in this podAssociationNamespace with this service account can assume the role 177 | for _, pod := range pods { 178 | if pod.ServiceAccount.Name == podAssociation.ServiceAccountName { 179 | assumableIamRole := AssumableIAMRole{ 180 | IAMRole: &IAMRole{Arn: *podAssociationDetails.Association.RoleArn}, 181 | Reason: AssumeIAMRoleReasonPodIdentity, 182 | } 183 | 184 | // Did we already find this role for this SA? (case where multiple pods have the same SA) 185 | if _, ok := serviceAccountsHandledForPodAssociation[pod.ServiceAccount.Name]; !ok { 186 | pod.ServiceAccount.AssumableRoles = append(pod.ServiceAccount.AssumableRoles, &assumableIamRole) 187 | serviceAccountsHandledForPodAssociation[pod.ServiceAccount.Name] = true 188 | } 189 | } 190 | } 191 | } 192 | } 193 | 194 | return nil 195 | } 196 | 197 | func (m *EKSCluster) AnalyzeRoleRelationshipsForIRSA() error { 198 | log.Println("Analyzing IAM Roles For Service Accounts (IRSA) configuration") 199 | if m.IssuerURL == "" { 200 | log.Println("Your cluster has no OIDC provider, skipping IRSA analysis") 201 | return nil 202 | } 203 | 204 | for _, role := range m.IAMRoles { 205 | // Parse the role trust policy 206 | trustPolicy, err := iam_evaluation.ParseRoleTrustPolicy(role.TrustPolicy) 207 | if err != nil { 208 | log.Println("[WARNING] Could not parse the trust policy of " + role.Arn + ", ignoring. Error: " + err.Error()) 209 | continue 210 | } 211 | 212 | assumableIamRole := AssumableIAMRole{ 213 | IAMRole: &IAMRole{Arn: role.Arn}, 214 | Reason: AssumeIAMRoleReasonIRSA, 215 | } 216 | 217 | // Iterate over all service accounts in the cluster and figure out which ones can assume the role 218 | for namespace, serviceAccounts := range m.ServiceAccountsByNamespace { 219 | for _, serviceAccount := range serviceAccounts { 220 | authzContext := iam_evaluation.AuthorizationContext{ 221 | Action: "sts:AssumeRoleWithWebIdentity", 222 | Principal: &iam_evaluation.Principal{ 223 | Type: iam_evaluation.PrincipalTypeFederated, 224 | ID: fmt.Sprintf("arn:aws:iam::%s:oidc-provider/%s", m.AccountID, m.IssuerURL), 225 | }, 226 | ContextKeys: map[string]string{ 227 | fmt.Sprintf("%s:sub", m.IssuerURL): fmt.Sprintf("system:serviceaccount:%s:%s", namespace, serviceAccount.Name), 228 | fmt.Sprintf("%s:aud", m.IssuerURL): "sts.amazonaws.com", 229 | }, 230 | } 231 | 232 | if *trustPolicy.Authorize(&authzContext) == iam_evaluation.AuthorizationResultAllow { 233 | serviceAccount.AssumableRoles = append(serviceAccount.AssumableRoles, &assumableIamRole) 234 | } 235 | } 236 | } 237 | } 238 | 239 | return nil 240 | } 241 | 242 | func (m *EKSCluster) retrieveClusterInformation() error { 243 | log.Println("Retrieving cluster information") 244 | clusterInfo, err := eks.NewFromConfig(*m.AwsClient).DescribeCluster(context.Background(), &eks.DescribeClusterInput{ 245 | Name: &m.Name, 246 | }) 247 | if err != nil { 248 | return fmt.Errorf("unable to retrieve cluster OIDC issuer: %v", err) 249 | } 250 | if clusterInfo.Cluster.Identity == nil || clusterInfo.Cluster.Identity.Oidc == nil { 251 | // The cluster has no OIDC provider 252 | m.IssuerURL = "" 253 | return nil 254 | } 255 | 256 | parsedClusterArn, _ := arn.Parse(*clusterInfo.Cluster.Arn) 257 | m.AccountID = parsedClusterArn.AccountID 258 | m.IssuerURL = strings.Replace(*clusterInfo.Cluster.Identity.Oidc.Issuer, "https://", "", 1) 259 | m.KubernetesVersion = *clusterInfo.Cluster.Version 260 | return nil 261 | } 262 | 263 | func (m *EKSCluster) retrieveIAMRoles() ([]*IAMRole, error) { 264 | log.Println("Listing roles in the AWS account") 265 | paginator := iam.NewListRolesPaginator(iam.NewFromConfig(*m.AwsClient, func(options *iam.Options) { 266 | options.Region = "us-east-1" 267 | }), &iam.ListRolesInput{}) 268 | allIAMRoles := []*IAMRole{} 269 | for paginator.HasMorePages() { 270 | roles, err := paginator.NextPage(context.Background()) 271 | if err != nil { 272 | return nil, fmt.Errorf("unable to list roles in the AWS account: %v", err) 273 | } 274 | for _, role := range roles.Roles { 275 | trustPolicy, err := url.PathUnescape(*role.AssumeRolePolicyDocument) 276 | if err != nil { 277 | return nil, err 278 | } 279 | role := IAMRole{ 280 | Arn: *role.Arn, 281 | TrustPolicy: trustPolicy, 282 | } 283 | allIAMRoles = append(allIAMRoles, &role) 284 | } 285 | } 286 | return allIAMRoles, nil 287 | } 288 | 289 | func (m *EKSCluster) retrieveServiceAccountsByNamespace() (map[string][]*K8sServiceAccount, error) { 290 | log.Println("Listing K8s service accounts in all namespaces") 291 | serviceAccountsByNamespace := make(map[string][]*K8sServiceAccount) 292 | serviceAccounts, err := m.K8sClient.CoreV1().ServiceAccounts("").List(context.Background(), v1.ListOptions{}) 293 | if err != nil { 294 | return nil, fmt.Errorf("unable to list K8s service accounts: %v", err) 295 | } 296 | for _, serviceAccount := range serviceAccounts.Items { 297 | namespace := serviceAccount.Namespace 298 | serviceAccountsByNamespace[namespace] = append(serviceAccountsByNamespace[namespace], &K8sServiceAccount{ 299 | Name: serviceAccount.Name, 300 | Namespace: serviceAccount.Namespace, 301 | Annotations: serviceAccount.Annotations, 302 | AssumableRoles: []*AssumableIAMRole{}, 303 | }) 304 | } 305 | return serviceAccountsByNamespace, nil 306 | } 307 | 308 | func (m *EKSCluster) getPodsByNamespace() (map[string][]*K8sPod, error) { 309 | pods, err := m.K8sClient.CoreV1().Pods("").List(context.Background(), v1.ListOptions{}) 310 | if err != nil { 311 | return nil, fmt.Errorf("unable to list K8s pods: %v", err) 312 | } 313 | podsByNamespace := make(map[string][]*K8sPod) 314 | for _, pod := range pods.Items { 315 | namespace := pod.Namespace 316 | var serviceAccount *K8sServiceAccount = nil 317 | candidateServiceAccounts := m.ServiceAccountsByNamespace[namespace] 318 | for _, candidateServiceAccount := range candidateServiceAccounts { 319 | if candidateServiceAccount.Name == pod.Spec.ServiceAccountName { 320 | serviceAccount = candidateServiceAccount 321 | break 322 | } 323 | } 324 | podsByNamespace[namespace] = append(podsByNamespace[namespace], &K8sPod{ 325 | Name: pod.Name, 326 | Namespace: namespace, 327 | ServiceAccount: serviceAccount, 328 | HasProjectedServiceAccountToken: hasProjectedServiceAccountToken(&pod), 329 | }) 330 | } 331 | 332 | return podsByNamespace, nil 333 | } 334 | 335 | func (m *EKSCluster) supportsPodIdentity() bool { 336 | currentVersion, err := version.NewVersion(m.KubernetesVersion) 337 | minimumVersion, err2 := version.NewVersion(PodIdentityMinSupportedK8sVersion) 338 | if err != nil || err2 != nil { 339 | log.Println("WARNING: Unable to parse cluster K8s version, assuming it's >= 1.24 and supports Pod Identity") 340 | log.Println("Error: " + err.Error() + " " + err2.Error()) 341 | return true 342 | } 343 | 344 | return currentVersion.GreaterThanOrEqual(minimumVersion) 345 | } 346 | 347 | func hasProjectedServiceAccountToken(pod *corev1.Pod) bool { 348 | for _, volume := range pod.Spec.Volumes { 349 | projectedVolume := volume.Projected 350 | if projectedVolume == nil || len(projectedVolume.Sources) == 0 { 351 | continue 352 | } 353 | for _, source := range projectedVolume.Sources { 354 | saSource := source.ServiceAccountToken 355 | if saSource == nil { 356 | continue 357 | } 358 | if saSource.Audience == "sts.amazonaws.com" { 359 | return true 360 | } 361 | } 362 | } 363 | return false 364 | } 365 | -------------------------------------------------------------------------------- /pkg/managed-kubernetes-auditing-toolkit/eks/secrets/aws_secrets.go: -------------------------------------------------------------------------------- 1 | package secrets 2 | 3 | import "regexp" 4 | 5 | type AwsSecretScanningResult struct { 6 | AccessKey string 7 | SecretKey string 8 | } 9 | 10 | var ( 11 | AwsAccessKeyPattern = regexp.MustCompile("^AKIA[0-9A-Z]{16}$") 12 | AwsSecretKeyPattern = regexp.MustCompile("^[A-Za-z0-9/+=]{40}$") 13 | HashPattern = regexp.MustCompile("^[0-9a-fA-F]{40}$") 14 | ) 15 | 16 | func FindAwsCredentialsInUnstructuredString(input string) *AwsSecretScanningResult { 17 | var result = &AwsSecretScanningResult{} 18 | 19 | accessKey := matchAwsAccessKey(input) 20 | if accessKey != nil { 21 | result.AccessKey = *accessKey 22 | } 23 | 24 | secretKey := matchAwsSecretKey(input) 25 | if secretKey != nil { 26 | result.SecretKey = *secretKey 27 | } 28 | return result 29 | } 30 | 31 | func match(regex *regexp.Regexp, input string) *string { 32 | tokens := regexp.MustCompile(`(?s)\s*[^a-zA-Z0-9./+)_-]+\s*`).Split(input, -1) 33 | for _, token := range tokens { 34 | if token == "" { 35 | continue 36 | } 37 | test := regex.FindStringIndex(token) 38 | if test != nil { 39 | matchedValue := token[test[0]:test[1]] 40 | return &matchedValue 41 | } 42 | } 43 | return nil 44 | } 45 | func matchAwsAccessKey(value string) *string { 46 | return match(AwsAccessKeyPattern, value) 47 | } 48 | 49 | func matchAwsSecretKey(value string) *string { 50 | result := match(AwsSecretKeyPattern, value) 51 | if result != nil && !HashPattern.MatchString(value) { 52 | return result 53 | } 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /pkg/managed-kubernetes-auditing-toolkit/eks/secrets/aws_secrets_detector.go: -------------------------------------------------------------------------------- 1 | package secrets 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/aws/aws-sdk-go-v2/aws" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "k8s.io/client-go/kubernetes" 9 | "log" 10 | "strconv" 11 | ) 12 | 13 | type SecretsDetector struct { 14 | AwsClient *aws.Config 15 | K8sClient *kubernetes.Clientset 16 | Namespace string // empty for all namespaces 17 | } 18 | 19 | type SecretInfo struct { 20 | Namespace string 21 | Type string 22 | Name string 23 | Value string 24 | } 25 | 26 | func (m *SecretsDetector) FindSecrets() ([]*SecretInfo, error) { 27 | var secrets []*SecretInfo 28 | 29 | log.Println("Searching for AWS secrets in ConfigMaps...") 30 | configMapCredentials, err := m.findCredentialsInConfigMaps() 31 | if err == nil { 32 | secrets = append(secrets, configMapCredentials...) 33 | } else { 34 | log.Println("[WARN] Unable to access ConfigMaps: " + err.Error()) 35 | } 36 | 37 | log.Println("Searching for AWS secrets in Secrets...") 38 | secretCredentials, err := m.findCredentialsInSecrets() 39 | if err == nil { 40 | secrets = append(secrets, secretCredentials...) 41 | } else { 42 | log.Println("[WARN] Unable to access Secrets: " + err.Error()) 43 | } 44 | 45 | log.Println("Searching for AWS secrets in Pod definitions...") 46 | podCredentials, err := m.findCredentialsInPodDefinitions() 47 | if err == nil { 48 | secrets = append(secrets, podCredentials...) 49 | } else { 50 | log.Println("[WARN] Unable to access Pod definitions: " + err.Error()) 51 | } 52 | 53 | return secrets, nil 54 | } 55 | 56 | func (m *SecretsDetector) findCredentialsInConfigMaps() ([]*SecretInfo, error) { 57 | configMaps, err := m.K8sClient.CoreV1().ConfigMaps(m.Namespace).List(context.Background(), metav1.ListOptions{}) 58 | if err != nil { 59 | return nil, fmt.Errorf("unable to list ConfigMaps: %v", err) 60 | } 61 | var secrets []*SecretInfo 62 | log.Println("Analyzing " + strconv.Itoa(len(configMaps.Items)) + " ConfigMaps...") 63 | for _, configMap := range configMaps.Items { 64 | configMapSecrets := findSecretsInSingleConfigMap(&configMap) 65 | secrets = append(secrets, configMapSecrets...) 66 | } 67 | return secrets, nil 68 | } 69 | 70 | func (m *SecretsDetector) findCredentialsInSecrets() ([]*SecretInfo, error) { 71 | k8sSecrets, err := m.K8sClient.CoreV1().Secrets(m.Namespace).List(context.Background(), metav1.ListOptions{}) 72 | if err != nil { 73 | return nil, fmt.Errorf("unable to list Secrets: %v", err) 74 | } 75 | var secrets []*SecretInfo 76 | log.Println("Analyzing " + strconv.Itoa(len(k8sSecrets.Items)) + " Secrets...") 77 | for _, k8sSecret := range k8sSecrets.Items { 78 | configMapSecrets := findSecretsInSingleSecret(&k8sSecret) 79 | secrets = append(secrets, configMapSecrets...) 80 | } 81 | return secrets, nil 82 | } 83 | 84 | func (m *SecretsDetector) findCredentialsInPodDefinitions() ([]*SecretInfo, error) { 85 | pods, err := m.K8sClient.CoreV1().Pods(m.Namespace).List(context.Background(), metav1.ListOptions{}) 86 | if err != nil { 87 | return nil, fmt.Errorf("unable to list Pods: %v", err) 88 | } 89 | var secrets []*SecretInfo 90 | log.Println("Analyzing " + strconv.Itoa(len(pods.Items)) + " Pod definitions...") 91 | for _, pod := range pods.Items { 92 | podSecrets := findSecretsInSinglePodDefinition(&pod) 93 | secrets = append(secrets, podSecrets...) 94 | } 95 | return secrets, nil 96 | } 97 | -------------------------------------------------------------------------------- /pkg/managed-kubernetes-auditing-toolkit/eks/secrets/aws_secrets_test.go: -------------------------------------------------------------------------------- 1 | package secrets 2 | 3 | import "testing" 4 | 5 | func TestDetectsAWSAccessKeys(t *testing.T) { 6 | scenarios := []struct { 7 | Name string 8 | Value string 9 | ShouldMatch bool 10 | }{ 11 | {"should not match a non-AWS access key", "foobar", false}, 12 | {"should match an AWS access key", "AKIAZ3MSJV4WYJDU2ZDX", true}, 13 | {"should match an AWS access key after tokenization", "foo = AKIAZ3MSJV4WYJDU2ZDX", true}, 14 | {"should match an AWS access key after tokenization (json)", `{"foo": "AKIAZ3MSJV4WYJDU2ZDX"}`, true}, 15 | {"should match an AWS access key after tokenization (yaml)", `foo:\n\tbar: AKIAZ3MSJV4WYJDU2ZDX`, true}, 16 | {"should not match something that looks like an AWS access key that's buried in something else", `HELLOAKIAZ3MSJV4WYJDU2ZDXWORLD`, false}, 17 | } 18 | 19 | for _, scenario := range scenarios { 20 | t.Run(scenario.Name, func(t *testing.T) { 21 | result := matchAwsAccessKey(scenario.Value) 22 | if result == nil && scenario.ShouldMatch { 23 | t.Errorf("Expected to match AWS access key, but didn't") 24 | } else if result != nil && !scenario.ShouldMatch { 25 | t.Errorf("Expected to not match AWS access key, but did") 26 | } 27 | // Otherwise, all good 28 | }) 29 | } 30 | } 31 | 32 | func TestDetectsAWSSecretKeys(t *testing.T) { 33 | scenarios := []struct { 34 | Name string 35 | Value string 36 | ShouldMatch bool 37 | }{ 38 | {"should not match a non-AWS secret key", "foobar", false}, 39 | {"should match an AWS access key", "E7TZzdyO/HQPgp97EzWicL5FsXBHiFEka9HbtK+S", true}, 40 | {"should match an AWS access key after tokenization", "foo = E7TZzdyO/HQPgp97EzWicL5FsXBHiFEka9HbtK+S", true}, 41 | {"should match an AWS access key after tokenization (json)", `{"foo": "E7TZzdyO/HQPgp97EzWicL5FsXBHiFEka9HbtK+S"}`, true}, 42 | {"should match an AWS access key after tokenization (yaml)", `foo:\n\tbar: E7TZzdyO/HQPgp97EzWicL5FsXBHiFEka9HbtK+S`, true}, 43 | {"should not match something that looks like an AWS access key that's buried in something else", `HELLOE7TZzdyO/HQPgp97EzWicL5FsXBHiFEka9HbtK+SWORLD`, false}, 44 | {"should not match something that looks like an AWS access key but is actually a SHA1 or similar hash", `B3E37C058E373AF3B1CA2C7C5BAE8051595EE985`, false}, 45 | } 46 | 47 | for _, scenario := range scenarios { 48 | t.Run(scenario.Name, func(t *testing.T) { 49 | result := matchAwsSecretKey(scenario.Value) 50 | if result == nil && scenario.ShouldMatch { 51 | t.Errorf("Expected to match AWS access key, but didn't") 52 | } else if result != nil && !scenario.ShouldMatch { 53 | t.Errorf("Expected to not match AWS access key, but did") 54 | } 55 | // Otherwise, all good 56 | }) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /pkg/managed-kubernetes-auditing-toolkit/eks/secrets/configmap.go: -------------------------------------------------------------------------------- 1 | package secrets 2 | 3 | import ( 4 | "fmt" 5 | v1 "k8s.io/api/core/v1" 6 | ) 7 | 8 | func findSecretsInSingleConfigMap(configMap *v1.ConfigMap) []*SecretInfo { 9 | var secrets []*SecretInfo 10 | var accessKeyInfo *SecretInfo 11 | var secretKeyInfo *SecretInfo 12 | for key, value := range configMap.Data { 13 | configMapSecrets := FindAwsCredentialsInUnstructuredString(value) 14 | if configMapSecrets.AccessKey != "" { 15 | accessKeyInfo = &SecretInfo{ 16 | Namespace: configMap.Namespace, 17 | Name: fmt.Sprintf("%s (key %s)", configMap.Name, key), 18 | Type: "ConfigMap", 19 | Value: configMapSecrets.AccessKey, 20 | } 21 | } 22 | if configMapSecrets.SecretKey != "" { 23 | secretKeyInfo = &SecretInfo{ 24 | Namespace: configMap.Namespace, 25 | Name: fmt.Sprintf("%s (key %s)", configMap.Name, key), 26 | Type: "ConfigMap", 27 | Value: configMapSecrets.SecretKey, 28 | } 29 | } 30 | if accessKeyInfo != nil && secretKeyInfo != nil { 31 | secrets = append(secrets, accessKeyInfo, secretKeyInfo) 32 | // start searching for a new set of credentials 33 | accessKeyInfo = nil 34 | secretKeyInfo = nil 35 | } 36 | } 37 | return secrets 38 | } 39 | -------------------------------------------------------------------------------- /pkg/managed-kubernetes-auditing-toolkit/eks/secrets/configmap_test.go: -------------------------------------------------------------------------------- 1 | package secrets 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | v1 "k8s.io/api/core/v1" 6 | "testing" 7 | ) 8 | 9 | func TestDetectsSecretsInConfigMaps(t *testing.T) { 10 | scenarios := []struct { 11 | Name string 12 | ConfigMap *v1.ConfigMap 13 | ShouldFindSecret bool 14 | MatchedSecrets []string 15 | }{ 16 | { 17 | Name: "no secrets", 18 | ConfigMap: &v1.ConfigMap{Data: map[string]string{"foo": "bar"}}, 19 | ShouldFindSecret: false, 20 | }, 21 | { 22 | Name: "something that looks like an AWS secret key but is within a longer string", 23 | ConfigMap: &v1.ConfigMap{Data: map[string]string{ 24 | "access_key": "AKIAZ3MSJV4WWNKWW5FG", 25 | "foo": "XXXHP8lBRs8X50F/0nCAXqEPQ95+jlG/0pLdlNui2XFXXX", 26 | }}, 27 | ShouldFindSecret: false, 28 | }, 29 | { 30 | Name: "only something that looks like an AWS secret key but without an access key", 31 | ConfigMap: &v1.ConfigMap{Data: map[string]string{ 32 | "foo": "HP8lBRs8X50F/0nCAXqEPQ95+jlG/0pLdlNui2XF", 33 | }}, 34 | ShouldFindSecret: false, 35 | }, 36 | { 37 | Name: "only something that looks like an AWS access key but without a secret key", 38 | ConfigMap: &v1.ConfigMap{Data: map[string]string{ 39 | "foo": "AKIAZ3MSJV4WWNKWW5FG", 40 | }}, 41 | ShouldFindSecret: false, 42 | }, 43 | { 44 | Name: "an access key and a secret key", 45 | ConfigMap: &v1.ConfigMap{Data: map[string]string{ 46 | "access_key": "AKIAZ3MSJV4WWNKWW5FG", 47 | "secret_key": "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", 48 | }}, 49 | ShouldFindSecret: true, 50 | MatchedSecrets: []string{ 51 | "AKIAZ3MSJV4WWNKWW5FG", 52 | "HP8lBRs8X50F/0nCAXqEPQ95+jlG/0pLdlNui2XF", 53 | }, 54 | }, 55 | { 56 | Name: "2 access keys and secret keys", 57 | ConfigMap: &v1.ConfigMap{Data: map[string]string{ 58 | "access_key1": "AKIAZ3MSJV4WWNKWW5FG", 59 | "secret_key1": "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", 60 | "access_key2": "AKIAZ3MSJV4WWNKWW5FH", 61 | "secret_key2": "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", 62 | }}, 63 | ShouldFindSecret: true, 64 | MatchedSecrets: []string{ 65 | "AKIAZ3MSJV4WWNKWW5FG", 66 | "HP8lBRs8X50F/0nCAXqEPQ95+jlG/0pLdlNui2XF", 67 | "AKIAZ3MSJV4WWNKWW5FH", 68 | "HP8lBRs8X50F/0nCAXqEPQ95+jlG/0pLdlNui2XG", 69 | }, 70 | }, 71 | { 72 | Name: "an access key and a secret key in config-like string", 73 | ConfigMap: &v1.ConfigMap{Data: map[string]string{ 74 | "my_config": ` 75 | access_key = AKIAZ3MSJV4WWNKWW5FG 76 | secret_key = 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 77 | `, 78 | }}, 79 | ShouldFindSecret: true, 80 | MatchedSecrets: []string{ 81 | "AKIAZ3MSJV4WWNKWW5FG", 82 | "HP8lBRs8X50F/0nCAXqEPQ95+jlG/0pLdlNui2XF", 83 | }, 84 | }, 85 | { 86 | Name: "an access key and a secret key in JSON string", 87 | ConfigMap: &v1.ConfigMap{Data: map[string]string{ 88 | "my_config": ` 89 | { 90 | "myapp": { 91 | "access_key": "AKIAZ3MSJV4WWNKWW5FG", 92 | "secret_key": "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" 93 | } 94 | } 95 | `, 96 | }}, 97 | ShouldFindSecret: true, 98 | MatchedSecrets: []string{ 99 | "AKIAZ3MSJV4WWNKWW5FG", 100 | "HP8lBRs8X50F/0nCAXqEPQ95+jlG/0pLdlNui2XF", 101 | }, 102 | }, 103 | } 104 | 105 | for _, scenario := range scenarios { 106 | t.Run(scenario.Name, func(t *testing.T) { 107 | result := findSecretsInSingleConfigMap(scenario.ConfigMap) 108 | if scenario.ShouldFindSecret && len(result) == 0 { 109 | t.Errorf("expected to find secrets, but found none") 110 | } 111 | if !scenario.ShouldFindSecret && len(result) > 0 { 112 | t.Errorf("expected to find no secrets, but found %d", len(result)) 113 | } 114 | if len(scenario.MatchedSecrets) == 0 { 115 | return // nothing to check further 116 | } 117 | var allFoundSecrets []string 118 | for _, secret := range result { 119 | allFoundSecrets = append(allFoundSecrets, secret.Value) 120 | } 121 | assert.ElementsMatch(t, allFoundSecrets, scenario.MatchedSecrets) 122 | }) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /pkg/managed-kubernetes-auditing-toolkit/eks/secrets/pod.go: -------------------------------------------------------------------------------- 1 | package secrets 2 | 3 | import ( 4 | "fmt" 5 | v1 "k8s.io/api/core/v1" 6 | ) 7 | 8 | func findSecretsInSinglePodDefinition(pod *v1.Pod) []*SecretInfo { 9 | var secrets []*SecretInfo 10 | for _, container := range pod.Spec.Containers { 11 | secrets = append(secrets, findSecretsInContainerDefinition(pod, &container)...) 12 | } 13 | for _, container := range pod.Spec.InitContainers { 14 | secrets = append(secrets, findSecretsInContainerDefinition(pod, &container)...) 15 | } 16 | return secrets 17 | } 18 | 19 | func findSecretsInContainerDefinition(pod *v1.Pod, container *v1.Container) []*SecretInfo { 20 | var secrets []*SecretInfo 21 | 22 | var accessKeyInfo *SecretInfo 23 | var secretKeyInfo *SecretInfo 24 | 25 | for _, env := range container.Env { 26 | foundCredentials := FindAwsCredentialsInUnstructuredString(env.Value) 27 | if foundCredentials.AccessKey != "" { 28 | accessKeyInfo = &SecretInfo{ 29 | Namespace: pod.Namespace, 30 | Name: fmt.Sprintf("%s (environment variable %s)", pod.Name, env.Name), 31 | Type: "Pod", 32 | Value: foundCredentials.AccessKey, 33 | } 34 | } 35 | if foundCredentials.SecretKey != "" { 36 | secretKeyInfo = &SecretInfo{ 37 | Namespace: pod.Namespace, 38 | Name: fmt.Sprintf("%s (environment variable %s)", pod.Name, env.Name), 39 | Type: "Pod", 40 | Value: foundCredentials.SecretKey, 41 | } 42 | } 43 | if accessKeyInfo != nil && secretKeyInfo != nil { 44 | secrets = append(secrets, accessKeyInfo, secretKeyInfo) 45 | // start searching for a new set of credentials 46 | accessKeyInfo = nil 47 | secretKeyInfo = nil 48 | } 49 | } 50 | 51 | return secrets 52 | } 53 | -------------------------------------------------------------------------------- /pkg/managed-kubernetes-auditing-toolkit/eks/secrets/pod_test.go: -------------------------------------------------------------------------------- 1 | package secrets 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | v1 "k8s.io/api/core/v1" 6 | "testing" 7 | ) 8 | 9 | func makeContainerEnv(env map[string]string) []v1.EnvVar { 10 | var containerEnv = make([]v1.EnvVar, 0, len(env)) 11 | for key, value := range env { 12 | containerEnv = append(containerEnv, v1.EnvVar{Name: key, Value: value}) 13 | } 14 | return containerEnv 15 | } 16 | 17 | func podWithEnvironmentVariables(env map[string]string) *v1.Pod { 18 | return &v1.Pod{Spec: v1.PodSpec{Containers: []v1.Container{{Env: makeContainerEnv(env)}}}} 19 | } 20 | 21 | func podWithEnvironmentVariablesInInitContainer(env map[string]string) *v1.Pod { 22 | return &v1.Pod{Spec: v1.PodSpec{Containers: []v1.Container{{Name: "foo"}}, InitContainers: []v1.Container{{Name: "bar", Env: makeContainerEnv(env)}}}} 23 | } 24 | 25 | func TestDetectsSecretsInPods(t *testing.T) { 26 | scenarios := []struct { 27 | Name string 28 | Pod *v1.Pod 29 | ShouldFindSecret bool 30 | MatchedSecrets []string 31 | }{ 32 | { 33 | Name: "no environment variables", 34 | Pod: podWithEnvironmentVariables(map[string]string{}), 35 | ShouldFindSecret: false, 36 | }, 37 | { 38 | Name: "no secrets", 39 | Pod: podWithEnvironmentVariables(map[string]string{"foo": "bar"}), 40 | ShouldFindSecret: false, 41 | }, 42 | { 43 | Name: "something that looks like an AWS secret key but is within a longer string", 44 | Pod: podWithEnvironmentVariables(map[string]string{ 45 | "my_id": "AKIAZ3MSJV4WWNKWW5FG", 46 | "my_string": "XXXHP8lBRs8X50F/0nCAXqEPQ95+jlG/0pLdlNui2XFXXX", 47 | }), 48 | ShouldFindSecret: false, 49 | }, 50 | { 51 | Name: "only something that looks like an AWS secret key but without an access key", 52 | Pod: podWithEnvironmentVariables(map[string]string{ 53 | "foo": "HP8lBRs8X50F/0nCAXqEPQ95+jlG/0pLdlNui2XF", 54 | }), 55 | ShouldFindSecret: false, 56 | }, 57 | { 58 | Name: "only something that looks like an AWS access key but without an secret key", 59 | Pod: podWithEnvironmentVariables(map[string]string{ 60 | "foo": "AKIAZ3MSJV4WWNKWW5FG", 61 | }), 62 | ShouldFindSecret: false, 63 | }, 64 | { 65 | Name: "an access key and a secret key", 66 | Pod: podWithEnvironmentVariables(map[string]string{ 67 | "access": "AKIAZ3MSJV4WWNKWW5FG", 68 | "secret": "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", 69 | }), 70 | ShouldFindSecret: true, 71 | MatchedSecrets: []string{ 72 | "AKIAZ3MSJV4WWNKWW5FG", 73 | "HP8lBRs8X50F/0nCAXqEPQ95+jlG/0pLdlNui2XF", 74 | }, 75 | }, 76 | { 77 | Name: "two containers with pieces of credentials should not match", 78 | Pod: &v1.Pod{Spec: v1.PodSpec{Containers: []v1.Container{ 79 | {Env: makeContainerEnv(map[string]string{"access": "AKIAZ3MSJV4WWNKWW5FG"})}, 80 | {Env: makeContainerEnv(map[string]string{"secret": "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"})}, 81 | }}}, 82 | ShouldFindSecret: false, 83 | }, 84 | { 85 | Name: "an access key and a secret key in an init container", 86 | Pod: podWithEnvironmentVariablesInInitContainer(map[string]string{ 87 | "access": "AKIAZ3MSJV4WWNKWW5FG", 88 | "secret": "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", 89 | }), 90 | ShouldFindSecret: true, 91 | MatchedSecrets: []string{ 92 | "AKIAZ3MSJV4WWNKWW5FG", 93 | "HP8lBRs8X50F/0nCAXqEPQ95+jlG/0pLdlNui2XF", 94 | }, 95 | }, 96 | } 97 | 98 | for _, scenario := range scenarios { 99 | t.Run(scenario.Name, func(t *testing.T) { 100 | result := findSecretsInSinglePodDefinition(scenario.Pod) 101 | if scenario.ShouldFindSecret && len(result) == 0 { 102 | t.Errorf("expected to find secrets, but found none") 103 | } 104 | if !scenario.ShouldFindSecret && len(result) > 0 { 105 | t.Errorf("expected to find no secrets, but found %d", len(result)) 106 | } 107 | if len(scenario.MatchedSecrets) == 0 { 108 | return // nothing to check further 109 | } 110 | var allFoundSecrets []string 111 | for _, secret := range result { 112 | allFoundSecrets = append(allFoundSecrets, secret.Value) 113 | } 114 | assert.ElementsMatch(t, allFoundSecrets, scenario.MatchedSecrets) 115 | }) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /pkg/managed-kubernetes-auditing-toolkit/eks/secrets/secret.go: -------------------------------------------------------------------------------- 1 | package secrets 2 | 3 | import ( 4 | "fmt" 5 | v1 "k8s.io/api/core/v1" 6 | ) 7 | 8 | func findSecretsInSingleSecret(secret *v1.Secret) []*SecretInfo { 9 | // TODO: The code is very similar to ConfigMaps, we should probably refactor it 10 | var secrets []*SecretInfo 11 | var accessKeyInfo *SecretInfo 12 | var secretKeyInfo *SecretInfo 13 | 14 | for key, value := range secret.Data { 15 | foundCredentials := FindAwsCredentialsInUnstructuredString(string(value)) 16 | if foundCredentials.AccessKey != "" { 17 | accessKeyInfo = &SecretInfo{ 18 | Namespace: secret.Namespace, 19 | Name: fmt.Sprintf("%s (key %s)", secret.Name, key), 20 | Type: "Secret", 21 | Value: foundCredentials.AccessKey, 22 | } 23 | } 24 | if foundCredentials.SecretKey != "" { 25 | secretKeyInfo = &SecretInfo{ 26 | Namespace: secret.Namespace, 27 | Name: fmt.Sprintf("%s (key %s)", secret.Name, key), 28 | Type: "Secret", 29 | Value: foundCredentials.SecretKey, 30 | } 31 | } 32 | if accessKeyInfo != nil && secretKeyInfo != nil { 33 | secrets = append(secrets, accessKeyInfo, secretKeyInfo) 34 | // start searching for a new set of credentials 35 | accessKeyInfo = nil 36 | secretKeyInfo = nil 37 | } 38 | 39 | } 40 | return secrets 41 | } 42 | -------------------------------------------------------------------------------- /pkg/managed-kubernetes-auditing-toolkit/eks/secrets/secret_test.go: -------------------------------------------------------------------------------- 1 | package secrets 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | v1 "k8s.io/api/core/v1" 6 | "testing" 7 | ) 8 | 9 | func TestDetectsSecretsInK8sSecrets(t *testing.T) { 10 | scenarios := []struct { 11 | Name string 12 | K8sSecret *v1.Secret 13 | ShouldFindSecret bool 14 | MatchedSecrets []string 15 | }{ 16 | { 17 | Name: "no secrets", 18 | K8sSecret: &v1.Secret{Data: map[string][]byte{"foo": []byte("bar")}}, 19 | ShouldFindSecret: false, 20 | }, 21 | { 22 | Name: "something that looks like an AWS secret key but is within a longer string", 23 | K8sSecret: &v1.Secret{Data: map[string][]byte{ 24 | "access_key": []byte("AKIAZ3MSJV4WWNKWW5FG"), 25 | "foo": []byte("XXXHP8lBRs8X50F/0nCAXqEPQ95+jlG/0pLdlNui2XFXXX"), 26 | }}, 27 | ShouldFindSecret: false, 28 | }, 29 | { 30 | Name: "only something that looks like an AWS secret key but without an access key", 31 | K8sSecret: &v1.Secret{Data: map[string][]byte{ 32 | "foo": []byte("HP8lBRs8X50F/0nCAXqEPQ95+jlG/0pLdlNui2XF"), 33 | }}, 34 | ShouldFindSecret: false, 35 | }, 36 | { 37 | Name: "only something that looks like an AWS access key but without a secret key", 38 | K8sSecret: &v1.Secret{Data: map[string][]byte{ 39 | "foo": []byte("AKIAZ3MSJV4WWNKWW5FG"), 40 | }}, 41 | ShouldFindSecret: false, 42 | }, 43 | { 44 | Name: "an access key and a secret key", 45 | K8sSecret: &v1.Secret{Data: map[string][]byte{ 46 | "access_key": []byte("AKIAZ3MSJV4WWNKWW5FG"), 47 | "secret_key": []byte("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"), 48 | }}, 49 | ShouldFindSecret: true, 50 | MatchedSecrets: []string{ 51 | "AKIAZ3MSJV4WWNKWW5FG", 52 | "HP8lBRs8X50F/0nCAXqEPQ95+jlG/0pLdlNui2XF", 53 | }, 54 | }, 55 | } 56 | 57 | for _, scenario := range scenarios { 58 | t.Run(scenario.Name, func(t *testing.T) { 59 | result := findSecretsInSingleSecret(scenario.K8sSecret) 60 | if scenario.ShouldFindSecret && len(result) == 0 { 61 | t.Errorf("expected to find secrets, but found none") 62 | } 63 | if !scenario.ShouldFindSecret && len(result) > 0 { 64 | t.Errorf("expected to find no secrets, but found %d", len(result)) 65 | } 66 | if len(scenario.MatchedSecrets) == 0 { 67 | return // nothing to check further 68 | } 69 | var allFoundSecrets []string 70 | for _, secret := range result { 71 | allFoundSecrets = append(allFoundSecrets, secret.Value) 72 | } 73 | assert.ElementsMatch(t, allFoundSecrets, scenario.MatchedSecrets) 74 | }) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /pkg/managed-kubernetes-auditing-toolkit/eks/types.go: -------------------------------------------------------------------------------- 1 | package eks 2 | -------------------------------------------------------------------------------- /pkg/managed-kubernetes-auditing-toolkit/eks/utils.go: -------------------------------------------------------------------------------- 1 | package eks 2 | --------------------------------------------------------------------------------