diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..52f4b72 --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,93 @@ +name: Go + +on: + push: + branches: + - master + schedule: + - cron: '0 3 * * *' + tags: + - "v1.[0-9]+.[0-9]+" + +jobs: + lint: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: "1.20" + + - name: Run make test + run: make test + + - name: Run make staticcheck + run: make staticcheck + + build: + needs: + - lint + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + matrix: + platform: ['darwin', 'linux', 'windows'] + steps: + - uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: "1.20" + + - name: Run make build-${{ matrix.platform }} + run: make build-${{ matrix.platform }} + + - name: Check outputs + run: find ./releases -type f + + - name: Cache builds + uses: actions/cache@v3 + with: + path: ./releases + key: ${{ runner.os }}-go-${{ matrix.platform }}-${{ hashFiles('**/go.sum') }}-${{ github.ref_name }} + + release: + # only work when it is a tagged release + # ref: https://docs.github.com/en/actions/learn-github-actions/expressions + if: ${{ github.ref_type == 'tag' && contains(github.ref, 'v1.') }} + needs: + - build + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v3 + + - name: Restore cache-darwin + uses: actions/cache@v3 + with: + path: ./releases + key: ${{ runner.os }}-go-darwin-${{ hashFiles('**/go.sum') }}-${{ github.ref_name }} + + - name: Restore cache-linux + uses: actions/cache@v3 + with: + path: ./releases + key: ${{ runner.os }}-go-linux-${{ hashFiles('**/go.sum') }}-${{ github.ref_name }} + + - name: Restore cache-windows + uses: actions/cache@v3 + with: + path: ./releases + key: ${{ runner.os }}-go-windows-${{ hashFiles('**/go.sum') }}-${{ github.ref_name }} + + - name: Check outputs + run: find ./releases -type f + + - name: Run make release + run: make release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..b39729d --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,20 @@ +# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. +# +# You can adjust the behavior by modifying this file. +# For more information, see: +# https://github.com/actions/stale +name: Mark stale issues and pull requests + +on: + schedule: + - cron: '30 2 * * *' + +jobs: + stale: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + + steps: + - uses: actions/stale@v7 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8994785 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Ignore vendor files +vendor/* + +# Ignore binary releases +releases/* + +# Ignore garbage +.DS_Store + +# ignore binary output +cloudtrail-cli diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0b46bba --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +# v1.0.0 / 2023-02-23 + +* Initial Release diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d8b00c5 --- /dev/null +++ b/Makefile @@ -0,0 +1,71 @@ +.PHONY: staticcheck dependency clean build release all + +PKGS := $(shell go list ./...) +GITVERSION := $(shell git describe --tags --abbrev=8) +LDFLAGS := -s -w + +default: build + +staticcheck: + @echo "Golang Staticcheck..." + @go install honnef.co/go/tools/cmd/staticcheck@2023.1.2 + @for i in $(PKGS); do echo $${i}; staticcheck $${i}; done + +test: + go version + go fmt ./... + go vet ./... + # go test -v ./... + +dependency: + go mod download + +build-linux-x86_64: + @echo "Creating Build for Linux (x86_64)..." + @CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="$(LDFLAGS)" -o ./releases/$(GITVERSION)/Linux-x86_64/cloudtrail-cli + @cp ./LICENSE ./releases/$(GITVERSION)/Linux-x86_64/LICENSE + @tar zcf ./releases/$(GITVERSION)/cloudtrail-cli-Linux-x86_64.tar.gz -C releases/$(GITVERSION)/Linux-x86_64 cloudtrail-cli LICENSE + +build-linux-arm64: + @echo "Creating Build for Linux (arm64)..." + @CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="$(LDFLAGS)" -o ./releases/$(GITVERSION)/Linux-arm64/cloudtrail-cli + @cp ./LICENSE ./releases/$(GITVERSION)/Linux-arm64/LICENSE + @tar zcf ./releases/$(GITVERSION)/cloudtrail-cli-Linux-arm64.tar.gz -C releases/$(GITVERSION)/Linux-arm64 cloudtrail-cli LICENSE + +build-darwin-x86_64: + @echo "Creating Build for macOS (x86_64)..." + @CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="$(LDFLAGS)" -o ./releases/$(GITVERSION)/Darwin-x86_64/cloudtrail-cli + @cp ./LICENSE ./releases/$(GITVERSION)/Darwin-x86_64/LICENSE + @tar zcf ./releases/$(GITVERSION)/cloudtrail-cli-Darwin-x86_64.tar.gz -C releases/$(GITVERSION)/Darwin-x86_64 cloudtrail-cli LICENSE + +build-darwin-arm64: + @echo "Creating Build for macOS (arm64)..." + @CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="$(LDFLAGS)" -o ./releases/$(GITVERSION)/Darwin-arm64/cloudtrail-cli + @cp ./LICENSE ./releases/$(GITVERSION)/Darwin-arm64/LICENSE + @tar zcf ./releases/$(GITVERSION)/cloudtrail-cli-Darwin-arm64.tar.gz -C releases/$(GITVERSION)/Darwin-arm64 cloudtrail-cli LICENSE + +build-windows-x86_64: + @echo "Creating Build for Windows (x86_64)..." + @CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="$(LDFLAGS)" -o ./releases/$(GITVERSION)/Windows-x86_64/cloudtrail-cli.exe + @cp ./LICENSE ./releases/$(GITVERSION)/Windows-x86_64/LICENSE.txt + @tar zcf ./releases/$(GITVERSION)/cloudtrail-cli-Windows-x86_64.tar.gz -C releases/$(GITVERSION)/Windows-x86_64 cloudtrail-cli.exe LICENSE.txt + +build-linux: build-linux-x86_64 build-linux-arm64 +build-darwin: build-darwin-x86_64 build-darwin-arm64 +build-windows: build-windows-x86_64 + +build: build-linux build-darwin build-windows + +clean: + @echo "Cleanup Releases..." + rm -rvf ./releases/* + +release: + @echo "Creating Releases..." + @curl -LO https://github.com/tcnksm/ghr/releases/download/v0.16.0/ghr_v0.16.0_linux_amd64.tar.gz + @tar --strip-components=1 -xvf ghr_v0.16.0_linux_amd64.tar.gz ghr_v0.16.0_linux_amd64/ghr + ./ghr -version + ./ghr -replace -recreate -token ${GITHUB_TOKEN} $(GITVERSION) releases/$(GITVERSION)/ + sha1sum releases/$(GITVERSION)/*.tar.gz > releases/$(GITVERSION)/SHA1SUM + +all: staticcheck dependency clean build diff --git a/README.md b/README.md new file mode 100644 index 0000000..0e2d838 --- /dev/null +++ b/README.md @@ -0,0 +1,81 @@ +# cloudtrail-cli + +[![GitHub Actions](https://github.com/guessi/cloudtrail-cli/actions/workflows/go.yml/badge.svg?branch=master)](https://github.com/guessi/cloudtrail-cli/actions/workflows/go.yml) +[![GoDoc](https://godoc.org/github.com/guessi/cloudtrail-cli?status.svg)](https://godoc.org/github.com/guessi/cloudtrail-cli) +[![Go Report Card](https://goreportcard.com/badge/github.com/guessi/cloudtrail-cli)](https://goreportcard.com/report/github.com/guessi/cloudtrail-cli) +[![GitHub release](https://img.shields.io/github/release/guessi/cloudtrail-cli.svg)](https://github.com/guessi/cloudtrail-cli/releases/latest) +[![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/guessi/cloudtrail-cli)](https://github.com/guessi/cloudtrail-cli/blob/master/go.mod) + +Blazing fast single purpose cli for CloudTrail log filtering, written in golang + +# Install + +Find the latest download URL from [release](https://github.com/guessi/cloudtrail-cli/releases) page. + +```bash +$ curl ${DOWNLOAD_URL} -o ./cloudtrail-cli +$ chmod +x ./cloudtrail-cli +$ mv /usr/local/bin/cloudtrail-cli +``` + +# Usage + +```bash +$ cloudtrail-cli --version +cloudtrail-cli version 1.0.0 +``` + +```bash +$ cloudtrail-cli --help +NAME: + cloudtrail-cli - Blazing fast single purpose cli for CloudTrail log filtering + +USAGE: + cloudtrail-cli [global options] command [command options] [arguments...] + +VERSION: + 1.0.0 + +COMMANDS: + help, h Shows a list of commands or help for one command + +GLOBAL OPTIONS: + --profile value, -p value + --region value, -r value + --start-time value, -s value Timestamp in 2023-01-01T00:00:00 format (UTC) + --end-time value, -e value Timestamp in 2023-01-01T00:00:00 format (UTC) + --event-id value Filter events with event id + --event-name value Filter events with event name + --event-source value Filter events with event source + --user-name value Filter events with user name + --read-only Filter events with ReadOnly=true (default: false) + --no-read-only Filter events with ReadOnly=false (default: false) + --max-results value, -n value (default: "20") + --error-only Filter events with errors (default: false) + --help, -h show help + --version, -v print the version +``` + +### Sample Usage Case + +```bash +$ cloudtrail-cli --start-time 2023-02-01T00:00:00 --end-time 2023-02-01T01:00:00 --event-name AssumeRole --max-results 10 --region us-east-1 --read-only ++--------------------------------------+------------+----------------------+----------+-------------------+-------------------------------+-------------------------------+-------------+-----------+----------+ +| EventId | EventName | EventTime | Username | EventSource | UserAgent | SourceIPAddress | AccessKeyId | ErrorCode | ReadOnly | ++--------------------------------------+------------+----------------------+----------+-------------------+-------------------------------+-------------------------------+-------------+-----------+----------+ +| 998a47f3-fb53-48e0-83f1-111111111111 | AssumeRole | 2023-02-01T00:58:28Z | - | sts.amazonaws.com | eks.amazonaws.com | eks.amazonaws.com | | | true | +| 56018bd8-d0f4-41d3-a718-111111111111 | AssumeRole | 2023-02-01T00:57:51Z | - | sts.amazonaws.com | internetmonitor.amazonaws.com | internetmonitor.amazonaws.com | | | true | +| be8672c8-7725-4503-8a66-111111111111 | AssumeRole | 2023-02-01T00:56:31Z | - | sts.amazonaws.com | eks.amazonaws.com | eks.amazonaws.com | | | true | +| d5f7ff3f-af90-4f05-9050-111111111111 | AssumeRole | 2023-02-01T00:55:22Z | - | sts.amazonaws.com | ssm.amazonaws.com | ssm.amazonaws.com | | | true | +| 5aee1df5-d78d-4cac-bc20-111111111111 | AssumeRole | 2023-02-01T00:43:24Z | - | sts.amazonaws.com | eks.amazonaws.com | eks.amazonaws.com | | | true | +| 1b10a941-ee4a-490a-bd5c-111111111111 | AssumeRole | 2023-02-01T00:41:25Z | - | sts.amazonaws.com | eks.amazonaws.com | eks.amazonaws.com | | | true | +| 139dd66c-d192-47fc-9158-111111111111 | AssumeRole | 2023-02-01T00:40:38Z | - | sts.amazonaws.com | lambda.amazonaws.com | lambda.amazonaws.com | | | true | +| 5b2e956f-b73c-49bd-8970-111111111111 | AssumeRole | 2023-02-01T00:40:21Z | - | sts.amazonaws.com | ssm.amazonaws.com | ssm.amazonaws.com | | | true | +| faa2fd13-f9b9-4276-90fa-111111111111 | AssumeRole | 2023-02-01T00:39:16Z | - | sts.amazonaws.com | eks.amazonaws.com | eks.amazonaws.com | | | true | +| 8af6dc45-fd58-4ad5-9e95-111111111111 | AssumeRole | 2023-02-01T00:35:06Z | - | sts.amazonaws.com | lambda.amazonaws.com | lambda.amazonaws.com | | | true | ++--------------------------------------+------------+----------------------+----------+-------------------+-------------------------------+-------------------------------+-------------+-----------+----------+ +``` + +# License + +[Apache-2.0](LICENSE) diff --git a/cmd/flags.go b/cmd/flags.go new file mode 100644 index 0000000..4cfe74b --- /dev/null +++ b/cmd/flags.go @@ -0,0 +1,77 @@ +package cmd + +import ( + "time" + + "github.com/urfave/cli/v2" +) + +var Flags = []cli.Flag{ + &cli.StringFlag{ + Name: "profile", + Aliases: []string{"p"}, + Required: false, + }, + &cli.StringFlag{ + Name: "region", + Aliases: []string{"r"}, + Required: false, + }, + &cli.TimestampFlag{ + Name: "start-time", + Aliases: []string{"s"}, + Layout: "2006-01-02T15:04:05", + Timezone: time.UTC, + Usage: "Timestamp in 2023-01-01T00:00:00 format (UTC)", + Required: false, + }, + &cli.TimestampFlag{ + Name: "end-time", + Aliases: []string{"e"}, + Layout: "2006-01-02T15:04:05", + Timezone: time.UTC, + Usage: "Timestamp in 2023-01-01T00:00:00 format (UTC)", + Required: false, + }, + &cli.StringFlag{ + Name: "event-id", + Usage: "Filter events with event id", + Required: false, + }, + &cli.StringFlag{ + Name: "event-name", + Usage: "Filter events with event name", + Required: false, + }, + &cli.StringFlag{ + Name: "event-source", + Usage: "Filter events with event source", + Required: false, + }, + &cli.StringFlag{ + Name: "user-name", + Usage: "Filter events with user name", + Required: false, + }, + &cli.BoolFlag{ + Name: "read-only", + Usage: "Filter events with ReadOnly=true", + Required: false, + }, + &cli.BoolFlag{ + Name: "no-read-only", + Usage: "Filter events with ReadOnly=false", + Required: false, + }, + &cli.StringFlag{ + Name: "max-results", + Aliases: []string{"n"}, + Value: "20", + Required: false, + }, + &cli.BoolFlag{ + Name: "error-only", + Usage: "Filter events with errors", + Required: false, + }, +} diff --git a/cmd/wrapper.go b/cmd/wrapper.go new file mode 100644 index 0000000..4f7f64d --- /dev/null +++ b/cmd/wrapper.go @@ -0,0 +1,23 @@ +package cmd + +import ( + "github.com/guessi/cloudtrail-cli/pkg/utils" + "github.com/urfave/cli/v2" +) + +func QueryHandlerWrapper(c *cli.Context) { + utils.EventsHandler( + c.String("profile"), + c.String("region"), + c.Timestamp("start-time"), + c.Timestamp("end-time"), + c.String("event-id"), + c.String("event-name"), + c.String("event-source"), + c.String("user-name"), + c.Bool("read-only"), + c.Bool("no-read-only"), + c.Int("max-results"), + c.Bool("error-only"), + ) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e3dd3ac --- /dev/null +++ b/go.mod @@ -0,0 +1,32 @@ +module github.com/guessi/cloudtrail-cli + +go 1.20 + +require ( + github.com/aws/aws-sdk-go-v2 v1.17.5 + github.com/aws/aws-sdk-go-v2/config v1.18.15 + github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.24.2 + github.com/google/uuid v1.3.0 + github.com/jedib0t/go-pretty/v6 v6.4.4 + github.com/urfave/cli/v2 v2.24.4 +) + +require ( + github.com/aws/aws-sdk-go-v2/credentials v1.13.15 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.23 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.23 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.3.30 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.23 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.12.4 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.4 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.18.5 // indirect + github.com/aws/smithy-go v1.13.5 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/stretchr/testify v1.8.0 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + golang.org/x/sys v0.5.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0a66221 --- /dev/null +++ b/go.sum @@ -0,0 +1,68 @@ +github.com/aws/aws-sdk-go-v2 v1.17.5 h1:TzCUW1Nq4H8Xscph5M/skINUitxM5UBAyvm2s7XBzL4= +github.com/aws/aws-sdk-go-v2 v1.17.5/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= +github.com/aws/aws-sdk-go-v2/config v1.18.15 h1:509yMO0pJUGUugBP2H9FOFyV+7Mz7sRR+snfDN5W4NY= +github.com/aws/aws-sdk-go-v2/config v1.18.15/go.mod h1:vS0tddZqpE8cD9CyW0/kITHF5Bq2QasW9Y1DFHD//O0= +github.com/aws/aws-sdk-go-v2/credentials v1.13.15 h1:0rZQIi6deJFjOEgHI9HI2eZcLPPEGQPictX66oRFLL8= +github.com/aws/aws-sdk-go-v2/credentials v1.13.15/go.mod h1:vRMLMD3/rXU+o6j2MW5YefrGMBmdTvkLLGqFwMLBHQc= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.23 h1:Kbiv9PGnQfG/imNI4L/heyUXvzKmcWSBeDvkrQz5pFc= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.23/go.mod h1:mOtmAg65GT1HIL/HT/PynwPbS+UG0BgCZ6vhkPqnxWo= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.29 h1:9/aKwwus0TQxppPXFmf010DFrE+ssSbzroLVYINA+xE= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.29/go.mod h1:Dip3sIGv485+xerzVv24emnjX5Sg88utCL8fwGmCeWg= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.23 h1:b/Vn141DBuLVgXbhRWIrl9g+ww7G+ScV5SzniWR13jQ= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.23/go.mod h1:mr6c4cHC+S/MMkrjtSlG4QA36kOznDep+0fga5L/fGQ= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.30 h1:IVx9L7YFhpPq0tTnGo8u8TpluFu7nAn9X3sUDMb11c0= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.30/go.mod h1:vsbq62AOBwQ1LJ/GWKFxX8beUEYeRp/Agitrxee2/qM= +github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.24.2 h1:78Vd89OAhyNAvnxYrvG+KDS1JRhAw9FVnqwUvRIv750= +github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.24.2/go.mod h1:TUyo0xj0z79cbIcLwESK9Toc/G2K4GGwMrBI+G7jvpk= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.23 h1:QoOybhwRfciWUBbZ0gp9S7XaDnCuSTeK/fySB99V1ls= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.23/go.mod h1:9uPh+Hrz2Vn6oMnQYiUi/zbh3ovbnQk19YKINkQny44= +github.com/aws/aws-sdk-go-v2/service/sso v1.12.4 h1:qJdM48OOLl1FBSzI7ZrA1ZfLwOyCYqkXV5lko1hYDBw= +github.com/aws/aws-sdk-go-v2/service/sso v1.12.4/go.mod h1:jtLIhd+V+lft6ktxpItycqHqiVXrPIRjWIsFIlzMriw= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.4 h1:YRkWXQveFb0tFC0TLktmmhGsOcCgLwvq88MC2al47AA= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.4/go.mod h1:zVwRrfdSmbRZWkUkWjOItY7SOalnFnq/Yg2LVPqDjwc= +github.com/aws/aws-sdk-go-v2/service/sts v1.18.5 h1:L1600eLr0YvTT7gNh3Ni24yGI7NSHkq9Gp62vijPRCs= +github.com/aws/aws-sdk-go-v2/service/sts v1.18.5/go.mod h1:1mKZHLLpDMHTNSYPJ7qrcnCQdHCWsNQaT0xRvq2u80s= +github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8= +github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jedib0t/go-pretty/v6 v6.4.4 h1:N+gz6UngBPF4M288kiMURPHELDMIhF/Em35aYuKrsSc= +github.com/jedib0t/go-pretty/v6 v6.4.4/go.mod h1:MgmISkTWDSFu0xOqiZ0mKNntMQ2mDgOcwOkwBEkMDJI= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/urfave/cli/v2 v2.24.4 h1:0gyJJEBYtCV87zI/x2nZCPyDxD51K6xM8SkwjHFCNEU= +github.com/urfave/cli/v2 v2.24.4/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..3108431 --- /dev/null +++ b/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "os" + + "github.com/guessi/cloudtrail-cli/cmd" + "github.com/guessi/cloudtrail-cli/pkg/constants" + "github.com/urfave/cli/v2" +) + +func main() { + app := &cli.App{ + Name: constants.NAME, + Usage: constants.USAGE, + Version: constants.VERSION, + Flags: cmd.Flags, + Action: func(c *cli.Context) error { + cmd.QueryHandlerWrapper(c) + return nil + }, + } + + err := app.Run(os.Args) + if err != nil { + os.Exit(1) + } +} diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go new file mode 100644 index 0000000..86abd21 --- /dev/null +++ b/pkg/constants/constants.go @@ -0,0 +1,7 @@ +package constants + +const ( + NAME string = "cloudtrail-cli" + USAGE string = "Blazing fast single purpose cli for CloudTrail log filtering" + VERSION string = "1.0.0" +) diff --git a/pkg/types/types.go b/pkg/types/types.go new file mode 100644 index 0000000..0823daf --- /dev/null +++ b/pkg/types/types.go @@ -0,0 +1,63 @@ +package types + +// References: +// - https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-event-reference.html +// - https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-event-reference-record-contents.html +// - https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-event-reference-user-identity.html + +// AssumedRole +type SessionIssuer struct { + Type string `json:"type"` + PrincipalId string `json:"principalId"` + Arn string `json:"arn"` + AccountId string `json:"accountId"` + UserName string `json:"userName"` +} + +type WebIdFederationData struct { + FederatedProvider string `json:"federatedProvider"` + Attributes interface{} `json:"attributes"` +} + +type Attributes struct { + CreationDate string `json:"creationDate"` + MfaAuthenticated string `json:"mfaAuthenticated"` +} + +type SessionContext struct { + SessionIssuer SessionIssuer `json:"sessionIssuer"` + WebIdFederationData WebIdFederationData `json:"webIdFederationData"` + Attributes Attributes `json:"attributes"` +} + +type UserIdentity struct { + Type string `json:"type"` + PrincipalId string `json:"principalId"` + Arn string `json:"arn"` + AccountId string `json:"accountId"` + AccessKeyId string `json:"accessKeyId"` + UserName string `json:"userName,omitempty"` + SessionContext SessionContext `json:"sessionContext,omitempty"` + InvokedBy string `json:"invokedBy"` +} + +type CloudTrailEvent struct { + EventVersion string `json:"eventVersion"` + UserIdentity UserIdentity `json:"userIdentity"` + EventTime string `json:"eventTime"` + EventSource string `json:"eventSource"` + EventName string `json:"eventName"` + AwsRegion string `json:"awsRegion"` + SourceIPAddress string `json:"sourceIPAddress"` + UserAgent string `json:"userAgent"` + ErrorCode string `json:"errorCode,omitempty"` + ErrorMessage string `json:"errorMessage,omitempty"` + RequestParameters interface{} `json:"requestParameters"` + ResponseElements interface{} `json:"responseElements"` + RequestId string `json:"requestID"` + EventId string `json:"eventID"` + ReadOnly bool `json:"readOnly"` + ManagementEvent bool `json:"managementEvent"` + RecipientAccountId string `json:"recipientAccountId"` + EventCategory string `json:"eventCategory"` +} diff --git a/pkg/utils/handlers.go b/pkg/utils/handlers.go new file mode 100644 index 0000000..1daa8ca --- /dev/null +++ b/pkg/utils/handlers.go @@ -0,0 +1,102 @@ +package utils + +import ( + "context" + "encoding/json" + "log" + "os" + "time" + + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/cloudtrail" + "github.com/guessi/cloudtrail-cli/pkg/types" + "github.com/jedib0t/go-pretty/v6/table" + "github.com/jedib0t/go-pretty/v6/text" +) + +func EventsHandler(profile, region string, startTime, endTime *time.Time, eventId, eventName, eventSource, userName string, readOnly, noReadOnly bool, maxResults int, errorOnly bool) { + // do nothing if maxResults is invalid input + if maxResults <= 0 { + log.Fatalf("Can not pass --max-results with a value lower or equal to 0.\n") + } + + // --read-only and --no-read-only should be mutually exclusive + if readOnly && noReadOnly { + log.Fatalf("Can not pass --read-only and --no-read-only at the same time.\n") + } + + cfg, err := config.LoadDefaultConfig( + context.TODO(), + config.WithRegion(region), + config.WithSharedConfigProfile(profile), + ) + if err != nil { + log.Fatalf("Unable to load SDK config. Error: %s\n", err.Error()) + } + + svc := cloudtrail.NewFromConfig(cfg) + + input := &cloudtrail.LookupEventsInput{ + StartTime: startTime, + EndTime: endTime, + LookupAttributes: composeLookupAttributesInput( + eventId, + eventName, + readOnly, + noReadOnly, + eventSource, + userName, + ), + MaxResults: getBatchSize(maxResults), + } + + events, err := LookupEvents(context.TODO(), svc, input, maxResults) + if err != nil { + log.Fatalf("Unable to ListTrails. Error: %s\n", err.Error()) + } + + t := table.NewWriter() + t.SetOutputMirror(os.Stdout) + t.AppendHeader(table.Row{ + "EventId", + "EventName", + "EventTime", + "Username", + "EventSource", + "UserAgent", + "SourceIPAddress", + "AccessKeyId", + "ErrorCode", + "ReadOnly", + }) + + for _, event := range events { + var c types.CloudTrailEvent + if err := json.Unmarshal([]byte(*event.CloudTrailEvent), &c); err != nil { + panic(err) + } + + // early exit if errorOnly flag is set + if errorOnly && len(c.ErrorCode) <= 0 { + continue + } + + username := getDisplayUserName(c.UserIdentity) + + t.AppendRow(table.Row{ + c.EventId, + c.EventName, + c.EventTime, + username, + c.EventSource, + c.UserAgent, + c.SourceIPAddress, + c.UserIdentity.AccessKeyId, + c.ErrorCode, + c.ReadOnly, + }) + } + + t.Style().Format.Header = text.FormatDefault + t.Render() +} diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go new file mode 100644 index 0000000..4ee9ae6 --- /dev/null +++ b/pkg/utils/utils.go @@ -0,0 +1,125 @@ +package utils + +import ( + "context" + "strings" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/cloudtrail" + ctypes "github.com/aws/aws-sdk-go-v2/service/cloudtrail/types" + "github.com/google/uuid" + "github.com/guessi/cloudtrail-cli/pkg/types" +) + +func isValidUUID(u string) bool { + _, err := uuid.Parse(u) + return err == nil +} + +func getDisplayUserName(u types.UserIdentity) string { + var username string + switch t := u.Type; t { + case "IAMUser": + username = u.UserName + case "WebIdentityUser": + username = u.UserName + case "AssumedRole": + username = strings.Split(u.Arn, "/")[2] + default: + username = "-" + } + return username +} + +func getBatchSize(i int) *int32 { + var defaultBatchSize int = 50 + var r int32 + if i > 0 && i <= defaultBatchSize { + r = int32(i) + } else { + r = int32(defaultBatchSize) + } + return &r +} + +func LookupEvents(ctx context.Context, svc *cloudtrail.Client, input *cloudtrail.LookupEventsInput, maxResults int) ([]ctypes.Event, error) { + var events []ctypes.Event + var returnSize int + + paginator := cloudtrail.NewLookupEventsPaginator(svc, input) + for paginator.HasMorePages() { + out, err := paginator.NextPage(ctx) + if err != nil { + return nil, err + } + events = append(events, out.Events...) + if len(events) > maxResults { + break + } + } + + returnSize = maxResults + if len(events) < maxResults { + returnSize = len(events) + } + + return events[:returnSize], nil +} + +func composeLookupAttributesInput(eventId, eventName string, readOnly, noReadOnly bool, eventSource, userName string) []ctypes.LookupAttribute { + lookupAttributesInput := []ctypes.LookupAttribute{} + + if isValidUUID(eventId) { + attrEventId := ctypes.LookupAttribute{ + AttributeKey: ctypes.LookupAttributeKeyEventId, + AttributeValue: aws.String(eventId), + } + lookupAttributesInput = append(lookupAttributesInput, attrEventId) + } + + if len(eventName) > 0 { + attrEventName := ctypes.LookupAttribute{ + AttributeKey: ctypes.LookupAttributeKeyEventName, + AttributeValue: aws.String(eventName), + } + lookupAttributesInput = append(lookupAttributesInput, attrEventName) + } + + var shouldPassReadonly bool + var lookupAttributeKeyReadOnlyValue *string + if readOnly != noReadOnly { + shouldPassReadonly = true + if readOnly { + lookupAttributeKeyReadOnlyValue = aws.String("true") + } + if noReadOnly { + lookupAttributeKeyReadOnlyValue = aws.String("false") + } + } + if shouldPassReadonly { + attrReadOnly := ctypes.LookupAttribute{ + AttributeKey: ctypes.LookupAttributeKeyReadOnly, + AttributeValue: lookupAttributeKeyReadOnlyValue, + } + lookupAttributesInput = append(lookupAttributesInput, attrReadOnly) + } + + const EVENT_SOURCE_SUFFIX = ".amazonaws.com" + if len(eventSource) > len(EVENT_SOURCE_SUFFIX) && strings.HasSuffix(eventSource, EVENT_SOURCE_SUFFIX) { + attrEventSource := ctypes.LookupAttribute{ + AttributeKey: ctypes.LookupAttributeKeyEventSource, + AttributeValue: aws.String(eventSource), + } + lookupAttributesInput = append(lookupAttributesInput, attrEventSource) + } + + if len(userName) > 0 { + attrUserName := ctypes.LookupAttribute{ + AttributeKey: ctypes.LookupAttributeKeyUsername, + AttributeValue: aws.String(userName), + } + lookupAttributesInput = append(lookupAttributesInput, attrUserName) + } + + return lookupAttributesInput +}