├── colors.go ├── .github ├── run-tests.sh ├── FUNDING.yml ├── workflows │ ├── push.yml │ ├── pull_request.yml │ ├── release.yml │ └── codeql-analysis.yml └── build ├── .gitignore ├── go.mod ├── tag2name └── tag2name.go ├── cmd_version.go ├── cmd_version_18.go ├── main.go ├── amiage └── amiage.go ├── cmd_orphaned_zones.go ├── cmd_ip.go ├── cmd_subnets.go ├── cmd_whoami.go ├── cmd_sg_grep.go ├── go.sum ├── utils └── utils.go ├── cmd_instances.go ├── cmd_stacks.go ├── instances └── instances.go ├── cmd_rotate_keys.go ├── cmd_csv_instances.go ├── cmd_whitelist_self.go ├── README.md └── LICENSE /colors.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var colorReset = "\033[0m" 4 | var colorRed = "\033[31m" 5 | -------------------------------------------------------------------------------- /.github/run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Run any test-scripts we have (none. oops) 4 | go test ./... 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | github: skx 3 | custom: https://steve.fi/donate/ 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | aws-utils-darwin-amd64 2 | aws-utils-freebsd-amd64 3 | aws-utils-freebsd-i386 4 | aws-utils-linux-amd64 5 | aws-utils-linux-i386 6 | aws-utils-windows-amd64.exe 7 | aws-utils-windows-i386.exe 8 | aws-utils 9 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/skx/aws-utils 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go v1.44.329 7 | github.com/pkg/errors v0.9.1 8 | github.com/skx/subcommands v0.9.2 9 | ) 10 | 11 | require github.com/jmespath/go-jmespath v0.4.0 // indirect 12 | -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | name: Push Event 6 | jobs: 7 | test: 8 | name: Test 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@master 12 | - name: Test 13 | uses: skx/github-action-tester@master 14 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | on: pull_request 2 | name: Pull Request 3 | jobs: 4 | golangci: 5 | name: lint 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/setup-go@v2 9 | - uses: actions/checkout@v2 10 | - name: golangci-lint 11 | uses: golangci/golangci-lint-action@v2 12 | test: 13 | name: Test 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@master 17 | - name: Test 18 | uses: skx/github-action-tester@master 19 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: [created] 4 | name: Handle Release 5 | jobs: 6 | upload: 7 | name: Upload 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout the repository 11 | uses: actions/checkout@master 12 | - name: Generate the artifacts 13 | uses: skx/github-action-build@master 14 | - name: Upload the artifacts 15 | uses: skx/github-action-publish-binaries@master 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | with: 19 | args: aws-utils-* 20 | -------------------------------------------------------------------------------- /tag2name/tag2name.go: -------------------------------------------------------------------------------- 1 | // Package tag2name contains a helper which will retrieve 2 | // the name of a "thing", by examining the values of each 3 | // tag. 4 | // 5 | // An AWS resource may contain numerous tags, but the name 6 | // of the thing will be retrieved from the tag named "Name". 7 | package tag2name 8 | 9 | import ( 10 | "github.com/aws/aws-sdk-go/service/ec2" 11 | ) 12 | 13 | // Lookup will return the name of a "thing" from the given set of tags. 14 | // If there is no name found the fallback value will be returned instead. 15 | func Lookup(tags []*ec2.Tag, fallback string) string { 16 | 17 | n := 0 18 | for n < len(tags) { 19 | 20 | if *tags[n].Key == "Name" { 21 | return *tags[n].Value 22 | } 23 | n++ 24 | } 25 | 26 | return fallback 27 | } 28 | -------------------------------------------------------------------------------- /cmd_version.go: -------------------------------------------------------------------------------- 1 | //go:build !go1.18 2 | // +build !go1.18 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/skx/subcommands" 10 | ) 11 | 12 | var ( 13 | version = "unreleased" 14 | ) 15 | 16 | // Structure for our options and state. 17 | type versionCommand struct { 18 | 19 | // We embed the NoFlags option, because we accept no command-line flags. 20 | subcommands.NoFlags 21 | } 22 | 23 | // Info returns the name of this subcommand. 24 | func (t *versionCommand) Info() (string, string) { 25 | return "version", `Show the version of this binary. 26 | 27 | Details: 28 | 29 | This reports upon the version of the application. 30 | ` 31 | } 32 | 33 | // Execute is invoked if the user specifies `version` as the subcommand. 34 | func (t *versionCommand) Execute(args []string) int { 35 | 36 | fmt.Printf("%s\n", version) 37 | 38 | return 0 39 | } 40 | -------------------------------------------------------------------------------- /cmd_version_18.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | // +build go1.18 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "runtime/debug" 9 | "strings" 10 | 11 | "github.com/skx/subcommands" 12 | ) 13 | 14 | var ( 15 | version = "unreleased" 16 | ) 17 | 18 | // Structure for our options and state. 19 | type versionCommand struct { 20 | 21 | // We embed the NoFlags option, because we accept no command-line flags. 22 | subcommands.NoFlags 23 | } 24 | 25 | // Info returns the name of this subcommand. 26 | func (t *versionCommand) Info() (string, string) { 27 | return "version", `Show the version of this binary. 28 | 29 | Details: 30 | 31 | This reports upon the version of the application. 32 | ` 33 | } 34 | 35 | // Execute is invoked if the user specifies `version` as the subcommand. 36 | func (t *versionCommand) Execute(args []string) int { 37 | 38 | fmt.Printf("%s\n", version) 39 | 40 | info, ok := debug.ReadBuildInfo() 41 | 42 | if ok { 43 | for _, settings := range info.Settings { 44 | if strings.Contains(settings.Key, "vcs") { 45 | fmt.Printf("%s: %s\n", settings.Key, settings.Value) 46 | } 47 | } 48 | } 49 | 50 | return 0 51 | } 52 | -------------------------------------------------------------------------------- /.github/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # The basename of our binary 4 | BASE="aws-utils" 5 | 6 | # 7 | # We build on multiple platforms/archs 8 | # 9 | BUILD_PLATFORMS="linux darwin freebsd windows" 10 | BUILD_ARCHS="amd64 386" 11 | 12 | # For each platform 13 | for OS in ${BUILD_PLATFORMS[@]}; do 14 | 15 | # For each arch 16 | for ARCH in ${BUILD_ARCHS[@]}; do 17 | 18 | # Setup a suffix for the binary 19 | SUFFIX="${OS}" 20 | 21 | # i386 is better than 386 22 | if [ "$ARCH" = "386" ]; then 23 | SUFFIX="${SUFFIX}-i386" 24 | else 25 | SUFFIX="${SUFFIX}-${ARCH}" 26 | fi 27 | 28 | # Windows binaries should end in .EXE 29 | if [ "$OS" = "windows" ]; then 30 | SUFFIX="${SUFFIX}.exe" 31 | fi 32 | 33 | echo "Building for ${OS} [${ARCH}] -> ${BASE}-${SUFFIX}" 34 | 35 | # Run the build 36 | export GOARCH=${ARCH} 37 | export GOOS=${OS} 38 | export CGO_ENABLED=0 39 | 40 | # Build the main-binary 41 | go build -ldflags "-X main.version=$(git describe --tags 2>/dev/null || echo 'master')" -o "${BASE}-${SUFFIX}" 42 | done 43 | done 44 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/skx/subcommands" 8 | ) 9 | 10 | // 11 | // Recovery is good 12 | // 13 | func recoverPanic() { 14 | if os.Getenv("DEBUG") != "" { 15 | return 16 | } 17 | 18 | if r := recover(); r != nil { 19 | fmt.Printf("recovered from panic while running %v\n%s\n", os.Args, r) 20 | fmt.Printf("To see the panic run 'export DEBUG=on' and repeat.\n") 21 | } 22 | } 23 | 24 | // 25 | // Register the subcommands, and run the one the user chose. 26 | // 27 | func main() { 28 | 29 | // 30 | // Catch errors 31 | // 32 | defer recoverPanic() 33 | 34 | // 35 | // Register each of our subcommands. 36 | // 37 | subcommands.Register(&csvInstancesCommand{}) 38 | subcommands.Register(&instancesCommand{}) 39 | subcommands.Register(&ipCommand{}) 40 | subcommands.Register(&orphanedZonesCommand{}) 41 | subcommands.Register(&rotateKeysCommand{}) 42 | subcommands.Register(&sgGrepCommand{}) 43 | subcommands.Register(&stacksCommand{}) 44 | subcommands.Register(&subnetsCommand{}) 45 | subcommands.Register(&whitelistSelfCommand{}) 46 | subcommands.Register(&versionCommand{}) 47 | subcommands.Register(&whoamiCommand{}) 48 | 49 | // 50 | // Execute the one the user chose. 51 | // 52 | os.Exit(subcommands.Execute()) 53 | } 54 | -------------------------------------------------------------------------------- /amiage/amiage.go: -------------------------------------------------------------------------------- 1 | // Package amiage contains a helper function to return the age of an 2 | // AMI in days. 3 | // 4 | // Caching is supported. 5 | package amiage 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "time" 11 | 12 | "github.com/aws/aws-sdk-go/aws" 13 | "github.com/aws/aws-sdk-go/service/ec2" 14 | ) 15 | 16 | // Cache of creation-time/date 17 | var cache map[string]string 18 | 19 | // NotFound is the error returned when an AMI isn't found 20 | var NotFound error 21 | 22 | // init ensures that our cache is initialized 23 | func init() { 24 | cache = make(map[string]string) 25 | NotFound = fmt.Errorf("not-found") 26 | } 27 | 28 | // AMICreation returns the creation-date of the given AMI as a string. 29 | // 30 | // Values are cached. 31 | func AMICreation(svc *ec2.EC2, id string) (string, error) { 32 | 33 | // Lookup in the cache to see if we've already found the creation 34 | // date for this AMI 35 | cached, ok := cache[id] 36 | if ok { 37 | return cached, nil 38 | } 39 | 40 | // Setup a filter for the AMI we're looking for. 41 | input := &ec2.DescribeImagesInput{ 42 | ImageIds: []*string{ 43 | aws.String(id), 44 | }, 45 | } 46 | 47 | // Run the search, return NotFound if we got an error. 48 | // 49 | // Yes we're losing the "real" error here, but logically we've not found the 50 | // details we want, so it's a not-found. 51 | result, err := svc.DescribeImages(input) 52 | if err != nil { 53 | return "", NotFound 54 | } 55 | 56 | // If we got a result then we can return the creation time (as a string) 57 | if len(result.Images) > 0 { 58 | 59 | // But save in a cache for the future 60 | date := *result.Images[0].CreationDate 61 | cache[id] = date 62 | return date, nil 63 | } 64 | 65 | // No result found 66 | return "", NotFound 67 | } 68 | 69 | // AMIAge returns the number of days since the specified image was created, 70 | // using AMICreation as a helper (that will cache the creation time). 71 | func AMIAge(svc *ec2.EC2, id string) (int, error) { 72 | 73 | // 74 | // Get the AMI creation-date 75 | // 76 | create, err := AMICreation(svc, id) 77 | if err != nil { 78 | 79 | // If this is "Not Found" then return -1 80 | if errors.Is(err, NotFound) { 81 | return -1, err 82 | } 83 | return -2, fmt.Errorf("failed to get creation date of %s: %s", id, err.Error()) 84 | } 85 | 86 | // 87 | // Parse the date, so we can report how many days 88 | // ago the AMI was created. 89 | // 90 | t, err := time.Parse("2006-01-02T15:04:05.000Z", create) 91 | if err != nil { 92 | return -3, fmt.Errorf("failed to parse time string %s: %s", create, err) 93 | } 94 | 95 | // 96 | // Count how old the AMI is in days 97 | // 98 | date := time.Now() 99 | diff := date.Sub(t) 100 | 101 | return (int(diff.Hours() / 24)), nil 102 | } 103 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | name: "CodeQL" 7 | 8 | on: 9 | push: 10 | branches: [master] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [master] 14 | schedule: 15 | - cron: '0 6 * * 4' 16 | 17 | jobs: 18 | analyze: 19 | name: Analyze 20 | runs-on: ubuntu-latest 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | # Override automatic language detection by changing the below list 26 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 27 | language: ['go'] 28 | # Learn more... 29 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v2 34 | with: 35 | # We must fetch at least the immediate parents so that if this is 36 | # a pull request then we can checkout the head. 37 | fetch-depth: 2 38 | 39 | # If this run was triggered by a pull request event, then checkout 40 | # the head of the pull request instead of the merge commit. 41 | - run: git checkout HEAD^2 42 | if: ${{ github.event_name == 'pull_request' }} 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /cmd_orphaned_zones.go: -------------------------------------------------------------------------------- 1 | // Show orphaned Route53 domains. 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "net" 8 | "sort" 9 | "strings" 10 | 11 | "github.com/aws/aws-sdk-go/service/route53" 12 | "github.com/skx/aws-utils/utils" 13 | "github.com/skx/subcommands" 14 | ) 15 | 16 | // Structure for our options and state. 17 | type orphanedZonesCommand struct { 18 | 19 | // We embed the NoFlags option, because we accept no command-line flags. 20 | subcommands.NoFlags 21 | } 22 | 23 | // Info returns the name of this subcommand. 24 | func (i *orphanedZonesCommand) Info() (string, string) { 25 | return "orphaned-zones", `Show orphaned Route53 zones. 26 | 27 | Details: 28 | 29 | This command retrieves a list of domains hosted on AWS Route53, and 30 | reports those which are orphaned. 31 | 32 | An orphaned domain is one which has all NS records pointing outside the 33 | AWS system. (Specifically we look for a NS record which does not contain 34 | the substring "aws" in its hostname.) 35 | ` 36 | 37 | } 38 | 39 | // Execute is invoked if the user specifies this sub-command. 40 | func (i *orphanedZonesCommand) Execute(args []string) int { 41 | 42 | // Start a session 43 | sess, err := utils.NewSession() 44 | if err != nil { 45 | fmt.Printf("%s\n", err.Error()) 46 | return 1 47 | } 48 | 49 | // Get the service handle 50 | svc := route53.New(sess) 51 | 52 | // Get all the results 53 | r, err := svc.ListHostedZones(&route53.ListHostedZonesInput{}) 54 | if err != nil { 55 | fmt.Printf("failed to call ListHostedZones: %s\n", err) 56 | return 1 57 | } 58 | 59 | // Collect orphans, errors, and valid domains in these 60 | // arrays so we can show them (sorted) at the end of our 61 | // processing 62 | valid := []string{} 63 | error := []string{} 64 | orphan := []string{} 65 | 66 | // Process each domain 67 | for _, entry := range r.HostedZones { 68 | 69 | // Lookup the nameservers, if there's an error skip 70 | nameserver, err := net.LookupNS(*entry.Name) 71 | if err != nil { 72 | error = append(error, fmt.Sprintf("%s - %s", *entry.Name, err)) 73 | continue 74 | } 75 | 76 | // Now we have the nameserver(s) look for ones that 77 | // contain the string "aws". This is a proxy for being 78 | // hosted by route53 (still). 79 | aws := true 80 | for _, ns := range nameserver { 81 | n := fmt.Sprintf("%s", ns) 82 | if !strings.Contains(n, "aws") { 83 | aws = false 84 | } 85 | } 86 | if aws { 87 | valid = append(valid, *entry.Name) 88 | } else { 89 | orphan = append(orphan, *entry.Name) 90 | } 91 | } 92 | 93 | // show results: valid, orphaned, error 94 | sort.Strings(valid) 95 | for _, entry := range valid { 96 | fmt.Printf("VALID - %s\n", entry) 97 | } 98 | sort.Strings(orphan) 99 | for _, entry := range orphan { 100 | fmt.Printf("ORPHAN - %s\n", entry) 101 | } 102 | sort.Strings(error) 103 | for _, entry := range error { 104 | fmt.Printf("ERROR - %s\n", entry) 105 | } 106 | 107 | return 0 108 | } 109 | -------------------------------------------------------------------------------- /cmd_ip.go: -------------------------------------------------------------------------------- 1 | // Show private IPv4 address of the named instance. 2 | // 3 | // Primarily written to handle tab-completion 4 | 5 | package main 6 | 7 | import ( 8 | "fmt" 9 | "flag" 10 | "regexp" 11 | 12 | "github.com/aws/aws-sdk-go/service/ec2" 13 | "github.com/skx/aws-utils/instances" 14 | "github.com/skx/aws-utils/utils" 15 | ) 16 | 17 | // Structure for our options and state. 18 | type ipCommand struct { 19 | 20 | // Are we verbose? 21 | verbose bool 22 | } 23 | 24 | 25 | // Arguments adds per-command args to the object. 26 | func (i *ipCommand) Arguments(f *flag.FlagSet) { 27 | f.BoolVar(&i.verbose, "verbose", false, "Should we show the matching name too?") 28 | } 29 | 30 | // Info returns the name of this subcommand. 31 | func (i *ipCommand) Info() (string, string) { 32 | return "ip", `Show the private IP of the given instance. 33 | 34 | Details: 35 | 36 | This command simply outputs the first private IP address of the instance 37 | which matches the given regular expression. 38 | 39 | $ aws-utils ip *prod*manager 40 | 10.12.43.120 41 | 42 | Unlike other commands this explicitly does not support the use of a role-path, 43 | being limited to the account signed in, and any assumed role only. 44 | 45 | It is useful for command-line completion, and similar scripting purposes.` 46 | 47 | } 48 | 49 | // DumpInstances looks up the appropriate details and outputs them to the 50 | // console, via the use of a provided template. 51 | func (i *ipCommand) OutputInformation(svc *ec2.EC2, acct string, void interface{}) error { 52 | 53 | // Get the name we're completing upon 54 | name := void.(string) 55 | 56 | // Get the instances that are running. 57 | ret, err := instances.GetInstances(svc, acct) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | // For each one, output the appropriate thing. 63 | for _, obj := range ret { 64 | 65 | // match against the name 66 | m, err := regexp.MatchString(name,obj.InstanceName) 67 | if err != nil { 68 | return fmt.Errorf("error running regexp match %s", err) 69 | } 70 | 71 | // if there was a match 72 | if m { 73 | if ( i.verbose ) { 74 | // show IP + name if being verbose 75 | fmt.Printf("%s %s\n", obj.PrivateIPv4, obj.InstanceName) 76 | } else { 77 | // otherwise just the IP. 78 | fmt.Printf("%s\n", obj.PrivateIPv4) 79 | } 80 | } 81 | } 82 | return nil 83 | } 84 | 85 | // Execute is invoked if the user specifies this subcommand. 86 | func (i *ipCommand) Execute(args []string) int { 87 | 88 | // 89 | // Get the connection, using default credentials 90 | // 91 | session, err := utils.NewSession() 92 | if err != nil { 93 | fmt.Printf("%s\n", err.Error()) 94 | return 1 95 | } 96 | 97 | for _, name := range args { 98 | 99 | // 100 | // Now invoke our callback which allows iteration over 101 | // available instances. 102 | // 103 | // Pass the name, but don't pass a role-path. 104 | // 105 | errs := utils.HandleRoles(session, "", i.OutputInformation, name) 106 | 107 | if len(errs) > 0 { 108 | fmt.Printf("errors encountered running this operation:\n") 109 | for _, err := range errs { 110 | fmt.Printf("%s\n", err) 111 | } 112 | return 1 113 | } 114 | } 115 | 116 | return 0 117 | } 118 | -------------------------------------------------------------------------------- /cmd_subnets.go: -------------------------------------------------------------------------------- 1 | // List the subnets available within the various accounts. 2 | 3 | package main 4 | 5 | import ( 6 | "flag" 7 | "fmt" 8 | 9 | "github.com/aws/aws-sdk-go/aws/awserr" 10 | "github.com/aws/aws-sdk-go/service/ec2" 11 | "github.com/skx/aws-utils/tag2name" 12 | "github.com/skx/aws-utils/utils" 13 | ) 14 | 15 | // Structure for our options and state. 16 | type subnetsCommand struct { 17 | 18 | // Path to a file containing roles 19 | rolesPath string 20 | 21 | // show the header already? 22 | header bool 23 | } 24 | 25 | // Arguments adds per-command args to the object. 26 | func (sc *subnetsCommand) Arguments(f *flag.FlagSet) { 27 | f.StringVar(&sc.rolesPath, "roles", "", "Path to a list of roles to process, one by one") 28 | } 29 | 30 | // Info returns the name of this subcommand. 31 | func (sc *subnetsCommand) Info() (string, string) { 32 | return "subnets", `List all subnets, and their names. 33 | 34 | Details: 35 | 36 | This command allows you to list the names of all subnets, and their 37 | associated CIDR ranges. All available VPCs will be exported in a 38 | simple CSV format, complete with header. 39 | ` 40 | 41 | } 42 | 43 | // Execute is invoked if the user specifies this sub-command. 44 | func (sc *subnetsCommand) Execute(args []string) int { 45 | 46 | // 47 | // Get the connection, using default credentials 48 | // 49 | session, err := utils.NewSession() 50 | if err != nil { 51 | fmt.Printf("%s\n", err.Error()) 52 | return 1 53 | } 54 | 55 | // 56 | // Now invoke our callback - this will call the function 57 | // "DisplaySubnets" once if we're not running with a role-file, 58 | // otherwise once for each role. 59 | // 60 | errs := utils.HandleRoles(session, sc.rolesPath, sc.DisplaySubnets, nil) 61 | 62 | if len(errs) > 0 { 63 | fmt.Printf("errors running display\n") 64 | 65 | for _, err := range errs { 66 | fmt.Printf("%s\n", err) 67 | } 68 | return 1 69 | } 70 | 71 | return 0 72 | } 73 | 74 | // DisplaySubnets is our callback method, which is invoked once for our main 75 | // account - if no roles-file is specified - or once for each assumed 76 | // role within that file. 77 | func (sc *subnetsCommand) DisplaySubnets(svc *ec2.EC2, account string, void interface{}) error { 78 | 79 | // An empty filter, to get all subnets 80 | input := &ec2.DescribeSubnetsInput{ 81 | Filters: []*ec2.Filter{ 82 | {}, 83 | }, 84 | } 85 | 86 | // describe the subnets 87 | result, err := svc.DescribeSubnets(input) 88 | if err != nil { 89 | if aerr, ok := err.(awserr.Error); ok { 90 | switch aerr.Code() { 91 | default: 92 | fmt.Println(aerr.Error()) 93 | } 94 | } else { 95 | // Print the error, cast err to awserr.Error to get the Code and 96 | // Message from an error. 97 | fmt.Println(err.Error()) 98 | } 99 | return fmt.Errorf("failed to get subnets for account %s", account) 100 | } 101 | 102 | // For each subnet 103 | for i := range result.Subnets { 104 | 105 | // Get the name, via tags, if present 106 | name := tag2name.Lookup(result.Subnets[i].Tags, "unnamed") 107 | 108 | // Show the details 109 | if !sc.header { 110 | fmt.Printf("Account, VPC, Subnet Name, Subnet ID, Cidr\n") 111 | sc.header = true 112 | } 113 | fmt.Printf("%s,%s,%s,%s,%s\n", account, *result.Subnets[i].VpcId, name, *result.Subnets[i].SubnetId, *result.Subnets[i].CidrBlock) 114 | } 115 | 116 | return nil 117 | } 118 | -------------------------------------------------------------------------------- /cmd_whoami.go: -------------------------------------------------------------------------------- 1 | // Show the current login details. 2 | // 3 | // Primarily written as a sanity-check when logging in via the sts:assumerole 4 | 5 | package main 6 | 7 | import ( 8 | "fmt" 9 | "os" 10 | "strings" 11 | 12 | "github.com/skx/aws-utils/utils" 13 | "github.com/skx/subcommands" 14 | 15 | "github.com/pkg/errors" 16 | 17 | "github.com/aws/aws-sdk-go/aws/awserr" 18 | "github.com/aws/aws-sdk-go/service/iam" 19 | "github.com/aws/aws-sdk-go/service/iam/iamiface" 20 | "github.com/aws/aws-sdk-go/service/sts" 21 | "github.com/aws/aws-sdk-go/service/sts/stsiface" 22 | ) 23 | 24 | // Structure for our options and state. 25 | type whoamiCommand struct { 26 | 27 | // We embed the NoFlags option, because we accept no command-line flags. 28 | subcommands.NoFlags 29 | } 30 | 31 | // Info returns the name of this subcommand. 32 | func (i *whoamiCommand) Info() (string, string) { 33 | return "whoami", `Show the current AWS user or role name. 34 | 35 | Details: 36 | 37 | This command shows you an overview of who you are current logged into 38 | AWS with, be it a root-user, or an assumed-role. 39 | ` 40 | 41 | } 42 | 43 | func showError() { 44 | txt := ` 45 | No credentials were found - Please see the following AWS link: 46 | 47 | https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html#cli-quick-configuration` 48 | fmt.Printf("%s\n", txt) 49 | os.Exit(1) 50 | } 51 | 52 | func getAccountID(svc stsiface.STSAPI) (id string) { 53 | callerID, err := svc.GetCallerIdentity(&sts.GetCallerIdentityInput{}) 54 | 55 | switch { 56 | case err != nil: 57 | if awsErr, okBPA2 := errors.Cause(err).(awserr.Error); okBPA2 { 58 | if strings.Contains(awsErr.Message(), "non-User credentials") { 59 | // not using user creds, so need to try a different method 60 | } else if awsErr.Code() == "NoCredentialProviders" { 61 | showError() 62 | } else if awsErr.Code() == "ExpiredToken" { 63 | fmt.Printf("Your temporary credentials have expired\n") 64 | os.Exit(1) 65 | } else if strings.Contains(awsErr.Message(), "security token included in the request is invalid") { 66 | fmt.Printf("The specified credentials have an invalid security token") 67 | os.Exit(1) 68 | } else { 69 | fmt.Printf("Unknown error using specified credentials: %s\n", awsErr.Message()) 70 | } 71 | } 72 | case callerID.Arn == nil: 73 | showError() 74 | default: 75 | id = *callerID.Account 76 | return 77 | } 78 | return id 79 | } 80 | 81 | func getAccountAlias(svc iamiface.IAMAPI) (alias string) { 82 | 83 | getAliasOutput, err := svc.ListAccountAliases(&iam.ListAccountAliasesInput{}) 84 | if err != nil { 85 | fmt.Println("Missing \"iam:ListAccountAliases\" permission so unable to retrieve alias") 86 | } else if len(getAliasOutput.AccountAliases) > 0 { 87 | alias = *getAliasOutput.AccountAliases[0] 88 | } 89 | return 90 | } 91 | 92 | // Execute is invoked if the user specifies this sub-command. 93 | func (i *whoamiCommand) Execute(args []string) int { 94 | 95 | // Start a session 96 | sess, err := utils.NewSession() 97 | if err != nil { 98 | fmt.Printf("%s\n", err.Error()) 99 | return 1 100 | } 101 | 102 | // Get the IAM handle, and STS service 103 | svc := iam.New(sess) 104 | stsSvc := sts.New(sess) 105 | 106 | // Find the account and alias (optional) 107 | accountID := getAccountID(stsSvc) 108 | accountAlias := getAccountAlias(svc) 109 | 110 | // Prefer the alias to the account 111 | if accountAlias != "" { 112 | fmt.Printf("%s\n", accountAlias) 113 | } else { 114 | fmt.Printf("%s\n", accountID) 115 | } 116 | return 0 117 | } 118 | -------------------------------------------------------------------------------- /cmd_sg_grep.go: -------------------------------------------------------------------------------- 1 | // Search security-groups. 2 | // 3 | // Primarily written as an everyday useful tool. 4 | 5 | package main 6 | 7 | import ( 8 | "flag" 9 | "fmt" 10 | "regexp" 11 | "strings" 12 | 13 | "github.com/aws/aws-sdk-go/service/ec2" 14 | "github.com/skx/aws-utils/utils" 15 | ) 16 | 17 | // Structure for our options and state. 18 | type sgGrepCommand struct { 19 | 20 | // Path to a file containing roles 21 | rolesPath string 22 | } 23 | 24 | // Arguments adds per-command args to the object. 25 | func (sg *sgGrepCommand) Arguments(f *flag.FlagSet) { 26 | f.StringVar(&sg.rolesPath, "roles", "", "Path to a list of roles to process, one by one") 27 | } 28 | 29 | // Info returns the name of this subcommand. 30 | func (sg *sgGrepCommand) Info() (string, string) { 31 | return "sg-grep", `Security-Group Grep 32 | 33 | Details: 34 | 35 | This command allows you to run grep against security-groups. 36 | ` 37 | 38 | } 39 | 40 | // Execute is invoked if the user specifies this sub-command. 41 | func (sg *sgGrepCommand) Execute(args []string) int { 42 | 43 | if len(args) < 1 { 44 | fmt.Printf("Usage: aws-utils sg-grep term1 ..\n") 45 | return 1 46 | } 47 | 48 | // 49 | // Get the connection, using default credentials 50 | // 51 | session, err := utils.NewSession() 52 | if err != nil { 53 | fmt.Printf("%s\n", err.Error()) 54 | return 1 55 | } 56 | 57 | // 58 | // Now invoke our callback - this will call the function 59 | // "Search" once if we're not running with a role-file, 60 | // otherwise once for each role. 61 | // 62 | errs := utils.HandleRoles(session, sg.rolesPath, sg.Search, args) 63 | 64 | if len(errs) > 0 { 65 | fmt.Printf("errors running search\n") 66 | 67 | for _, err := range errs { 68 | fmt.Printf("%s\n", err) 69 | } 70 | return 1 71 | } 72 | 73 | return 0 74 | } 75 | 76 | // Search is our callback method, which is invoked once for our main 77 | // account - if no roles-file is specified - or once for each assumed 78 | // role within that file. 79 | // 80 | // We return our search-terms to their array-form, and perform a single 81 | // search for each one. 82 | func (sg *sgGrepCommand) Search(svc *ec2.EC2, account string, void interface{}) error { 83 | 84 | // Get our search-terms back as an array 85 | terms := void.([]string) 86 | 87 | // For each one .. 88 | for _, term := range terms { 89 | 90 | // Run the search 91 | err := sg.searchTerm(svc, account, term) 92 | 93 | // return any error 94 | if err != nil { 95 | return err 96 | } 97 | } 98 | return nil 99 | } 100 | 101 | // searchTerm runs the search within one AWS account, and reports upon 102 | // any matches 103 | func (sg *sgGrepCommand) searchTerm(svc *ec2.EC2, account string, term string) error { 104 | 105 | // Compile the term into a regular expression 106 | // 107 | // NOTE: We add the `(?i)` prefix, to make this case insensitive. 108 | r, err := regexp.Compile("(?i)" + term) 109 | if err != nil { 110 | return fmt.Errorf("unable to compile regexp %s - %s", term, err) 111 | 112 | } 113 | 114 | // Retrieve the security groups 115 | result, err := svc.DescribeSecurityGroups(&ec2.DescribeSecurityGroupsInput{}) 116 | if err != nil { 117 | return fmt.Errorf("unable to get security-groups %s", err) 118 | } 119 | 120 | // For each security-group we find. 121 | for _, group := range result.SecurityGroups { 122 | 123 | // Get the contents as a string. 124 | txt := group.String() 125 | 126 | // If the string matches our regular expression we're good. 127 | if r.MatchString(txt) { 128 | 129 | // Show ID + description 130 | fmt.Printf("AWS Account:%s %s - %s\n", account, *group.GroupId, *group.Description) 131 | 132 | // Show contents of the SG - with a leading TAB 133 | lines := strings.Split(txt, "\n") 134 | for _, line := range lines { 135 | fmt.Printf("\t%s\n", line) 136 | } 137 | 138 | } 139 | } 140 | 141 | return nil 142 | } 143 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/aws/aws-sdk-go v1.44.329 h1:Rqy+wYI8h+iq+FphR59KKTsHR1Lz7YiwRqFzWa7xoYU= 2 | github.com/aws/aws-sdk-go v1.44.329/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= 3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 6 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 7 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 8 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 9 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 10 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 11 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 12 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 13 | github.com/skx/subcommands v0.9.2 h1:wG035k1U7Fn6A0hwOMg1ly7085cl62gnzLY1j78GISo= 14 | github.com/skx/subcommands v0.9.2/go.mod h1:HpOZHVUXT5Rc/Q7UCiyj7h5u6BleDfFjt+vxy2igonA= 15 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 16 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 17 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 18 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 19 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 20 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 21 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 22 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 23 | golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= 24 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 25 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 26 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 27 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 28 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 29 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 30 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 31 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 32 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 33 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 34 | golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 35 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 36 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 37 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 38 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 39 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 40 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 41 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 42 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 43 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 44 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 45 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 46 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | // Package utils just contains a couple of helper-methods which simplify 2 | // the implementation of our various sub-commands. 3 | package utils 4 | 5 | import ( 6 | "bufio" 7 | "fmt" 8 | "os" 9 | "strings" 10 | 11 | "github.com/aws/aws-sdk-go/aws/request" 12 | "github.com/aws/aws-sdk-go/aws/session" 13 | 14 | "github.com/aws/aws-sdk-go/aws" 15 | "github.com/aws/aws-sdk-go/aws/credentials/stscreds" 16 | "github.com/aws/aws-sdk-go/service/ec2" 17 | "github.com/aws/aws-sdk-go/service/sts" 18 | ) 19 | 20 | // AWSCallback is the signture of a function which can be invoked 21 | // against either the default AWS account, or against a list of 22 | // named roles assumed from it, in order. 23 | type AWSCallback func(svc *ec2.EC2, account string, void interface{}) error 24 | 25 | // NewSession returns an AWS session object, with optional request-tracing. 26 | // 27 | // If the environmental variable "DEBUG" is non-empty then requests made to 28 | // AWS will be logged to the console. 29 | func NewSession() (*session.Session, error) { 30 | 31 | sess, err := session.NewSession() 32 | if err != nil { 33 | return sess, err 34 | } 35 | 36 | debug := os.Getenv("DEBUG") 37 | if debug != "" { 38 | 39 | // Add a logging handler 40 | sess.Handlers.Send.PushFront(func(r *request.Request) { 41 | fmt.Printf("Request: %v [%s] %v", 42 | r.Operation, r.ClientInfo.ServiceName, r.Params) 43 | }) 44 | } 45 | 46 | return sess, nil 47 | } 48 | 49 | // HandleRoles invokes the specified callback, handling the case where 50 | // a role-file is specified or not. 51 | // 52 | // If the roleFile is empty then the function will be invoked once, 53 | // otherwise it will be invoked for every role. 54 | // 55 | // To allow execution to continue on subsequent roles errors in the execution 56 | // of a callback do not cause processing of the callback to terminate. 57 | func HandleRoles(session *session.Session, roleFile string, callback AWSCallback, void interface{}) []error { 58 | 59 | // We collect errors, and continue operating 60 | var errs []error 61 | 62 | // 63 | // Create a new session to find our account 64 | // 65 | svc := sts.New(session) 66 | out, err := svc.GetCallerIdentity(&sts.GetCallerIdentityInput{}) 67 | if err != nil { 68 | return []error{fmt.Errorf("failed to get identity: %s", err)} 69 | } 70 | 71 | // 72 | // This is our (default) account ID 73 | // 74 | acct := *out.Account 75 | 76 | // 77 | // If we have no role-list then just run the callback once, 78 | // using the default credentials. 79 | // 80 | if roleFile == "" { 81 | 82 | // Get the EC2 helper 83 | svc := ec2.New(session) 84 | 85 | // Run the callback 86 | err = callback(svc, acct, void) 87 | 88 | // append the error, if we got it. 89 | if err != nil { 90 | errs = append(errs, fmt.Errorf("error invoking callback: %s", err)) 91 | } 92 | 93 | // return any errors we received 94 | return errs 95 | } 96 | 97 | // 98 | // OK we have a list of roles, handle them one by one 99 | // 100 | file, err := os.Open(roleFile) 101 | if err != nil { 102 | return []error{fmt.Errorf("failed to open role-file %s: %s", roleFile, err)} 103 | } 104 | defer file.Close() 105 | 106 | // 107 | // Process the role-file line by line 108 | // 109 | scanner := bufio.NewScanner(file) 110 | for scanner.Scan() { 111 | 112 | // Get the line, and trim leading/trailing spaces 113 | role := scanner.Text() 114 | role = strings.TrimSpace(role) 115 | 116 | // Skip comments 117 | if strings.HasPrefix(role, "#") { 118 | continue 119 | } 120 | 121 | // Skip lines that aren't well-formed 122 | if !strings.HasPrefix(role, "arn:") && !strings.HasPrefix(role, "ARN:") { 123 | continue 124 | } 125 | 126 | // Assume the role 127 | creds := stscreds.NewCredentials(session, role) 128 | 129 | // Create service client value configured for credentials 130 | // from assumed role. 131 | svc := ec2.New(session, &aws.Config{Credentials: creds}) 132 | 133 | // We'll get the account from the string which looks like this: 134 | // 135 | // arn:aws:iam::1234:role/blah-abc 136 | // 137 | // We split by ":" and get the fourth field. 138 | // 139 | data := strings.Split(role, ":") 140 | acct := data[4] 141 | 142 | // invoke the callback for this account. 143 | err := callback(svc, acct, void) 144 | 145 | // If we got an error keep going, but save it away. 146 | if err != nil { 147 | errs = append(errs, fmt.Errorf("error invoking callback: %s", err)) 148 | } 149 | } 150 | 151 | // 152 | // Error processing the end of the file? 153 | // 154 | if err := scanner.Err(); err != nil { 155 | errs = append(errs, fmt.Errorf("error processing role-file: %s %s", roleFile, err)) 156 | } 157 | 158 | // return any errors we've built up 159 | return errs 160 | } 161 | -------------------------------------------------------------------------------- /cmd_instances.go: -------------------------------------------------------------------------------- 1 | // Show details of running instances, along with their volumes. 2 | // 3 | // Primarily written to answer support-questions. 4 | 5 | package main 6 | 7 | import ( 8 | "encoding/json" 9 | "flag" 10 | "fmt" 11 | "os" 12 | "text/template" 13 | 14 | "github.com/aws/aws-sdk-go/service/ec2" 15 | "github.com/skx/aws-utils/instances" 16 | "github.com/skx/aws-utils/utils" 17 | ) 18 | 19 | // Structure for our options and state. 20 | type instancesCommand struct { 21 | 22 | // Path to a file containing roles 23 | rolesPath string 24 | 25 | // Should we export our results in JSON format? 26 | jsonOutput bool 27 | 28 | // Should we dump the default template? 29 | dumpTemplate bool 30 | 31 | // Specify a non-default template? 32 | templatePath string 33 | } 34 | 35 | // Arguments adds per-command args to the object. 36 | func (i *instancesCommand) Arguments(f *flag.FlagSet) { 37 | f.StringVar(&i.rolesPath, "roles", "", "Path to a list of roles to process, one by one.") 38 | f.StringVar(&i.templatePath, "template", "", "Path to a template to render, instead of the default") 39 | f.BoolVar(&i.dumpTemplate, "dump-template", false, "Output the standard template to the console, and terminate") 40 | f.BoolVar(&i.jsonOutput, "json", false, "Output the results in JSON.") 41 | } 42 | 43 | // Info returns the name of this subcommand. 44 | func (i *instancesCommand) Info() (string, string) { 45 | return "instances", `Export a summary of running instances. 46 | 47 | Details: 48 | 49 | This command exports details about running instances, in a human-readable 50 | fashion. For example this is how a single instance is described by default: 51 | 52 | aviatrix-gateway - i-012345679abcdef01 53 | ----------------------------------------------------------- 54 | AMI : ami-0d3ba21723ec0dc5d 55 | AMI Age : 3 days 56 | Instance type: t2.small 57 | Public IPv4 address: 3.127.201.130 58 | Private IPv4 address: 10.10.3.78 59 | Volumes: 60 | /dev/sda1 vol-05c23836682aceab8 Size:16GiB IOPS:100 61 | 62 | The output of this command is generated by a standard text-template, and 63 | if you wish to customize the output you can specify the path to a template 64 | to use. 65 | 66 | The default template can be displayed by running: 67 | 68 | $ aws-utils instances -dump-template 69 | 70 | With the default template you can make your changes and use it like so: 71 | 72 | $ aws-utils instances -dump-template > foo.tmpl 73 | $ vi foo.tmpl 74 | $ aws-utils instances -template=./foo.tmpl 75 | .. 76 | ` 77 | 78 | } 79 | 80 | // DumpInstances looks up the appropriate details and outputs them to the 81 | // console, via the use of a provided template. 82 | func (i *instancesCommand) DumpInstances(svc *ec2.EC2, acct string, void interface{}) error { 83 | 84 | // Cast our template back into the correct object-type 85 | tmpl := void.(*template.Template) 86 | 87 | // Get the instances that are running. 88 | ret, err := instances.GetInstances(svc, acct) 89 | if err != nil { 90 | return err 91 | } 92 | 93 | // For each one, output the appropriate thing. 94 | for _, obj := range ret { 95 | 96 | // Output the rendered template to the console 97 | if i.jsonOutput { 98 | 99 | var b []byte 100 | b, err = json.Marshal(obj) 101 | if err != nil { 102 | return fmt.Errorf("error exporting to JSON %s", err) 103 | } 104 | fmt.Println(string(b)) 105 | } else { 106 | err = tmpl.Execute(os.Stdout, obj) 107 | if err != nil { 108 | return fmt.Errorf("error rendering template %s", err) 109 | } 110 | } 111 | } 112 | return nil 113 | } 114 | 115 | // Execute is invoked if the user specifies this subcommand. 116 | func (i *instancesCommand) Execute(args []string) int { 117 | 118 | // 119 | // Create the template we'll use for output 120 | // 121 | text := ` 122 | {{.InstanceName}} {{.InstanceID}} 123 | AMI : {{.InstanceAMI}} 124 | AMI Age : {{.AMIAge}} days 125 | AWS Account : {{.AWSAccount}} 126 | {{- if .SSHKeyName }} 127 | KeyName : {{.SSHKeyName}} 128 | {{- end}} 129 | {{- if .PrivateIPv4 }} 130 | Private IPv4: {{.PrivateIPv4}} 131 | {{- end}} 132 | {{- if .PublicIPv4 }} 133 | Public IPv4: {{.PublicIPv4}} 134 | {{- end}} 135 | {{if .Volumes}} 136 | Volumes:{{range .Volumes}} 137 | {{.Device}} {{.ID}} Size:{{.Size}}GiB Type:{{.Type}} Encrypted:{{.Encrypted}} IOPS:{{.IOPS}}{{end}} 138 | {{end}} 139 | ` 140 | 141 | // Show the template? 142 | if i.dumpTemplate { 143 | fmt.Printf("%s\n", text) 144 | return 0 145 | } 146 | 147 | // If the user specified a template-path then use it 148 | if i.templatePath != "" { 149 | content, err := os.ReadFile(i.templatePath) 150 | if err != nil { 151 | fmt.Printf("failed to read %s:%s\n", i.templatePath, err.Error()) 152 | return 1 153 | 154 | } 155 | 156 | text = string(content) 157 | } 158 | 159 | // Compile the template 160 | tmpl, err := template.New("output").Parse(text) 161 | if err != nil { 162 | fmt.Printf("failed to compile template:%s\n", err.Error()) 163 | return 1 164 | } 165 | 166 | // 167 | // Get the connection, using default credentials 168 | // 169 | session, err := utils.NewSession() 170 | if err != nil { 171 | fmt.Printf("%s\n", err.Error()) 172 | return 1 173 | } 174 | 175 | // 176 | // Now invoke our callback - this will call the function 177 | // "DumpInstances" once if we're not running with a role-file, 178 | // otherwise once for each role. 179 | // 180 | errs := utils.HandleRoles(session, i.rolesPath, i.DumpInstances, tmpl) 181 | 182 | if len(errs) > 0 { 183 | fmt.Printf("errors running instance dump\n") 184 | for _, err := range errs { 185 | fmt.Printf("%s\n", err) 186 | } 187 | return 1 188 | } 189 | 190 | return 0 191 | } 192 | -------------------------------------------------------------------------------- /cmd_stacks.go: -------------------------------------------------------------------------------- 1 | // List the names of each cloudformation stack 2 | 3 | package main 4 | 5 | import ( 6 | "flag" 7 | "fmt" 8 | "os" 9 | "regexp" 10 | "sort" 11 | "strings" 12 | 13 | "github.com/aws/aws-sdk-go/aws" 14 | "github.com/aws/aws-sdk-go/aws/session" 15 | "github.com/aws/aws-sdk-go/service/cloudformation" 16 | "github.com/aws/aws-sdk-go/service/ec2" 17 | "github.com/skx/aws-utils/utils" 18 | ) 19 | 20 | // Structure for our options and state. 21 | type stacksCommand struct { 22 | 23 | // filter allows filtering the returned stack-names 24 | filter string 25 | 26 | // Path to a file containing roles 27 | rolesPath string 28 | 29 | // policyPath is the policy-file to apply. 30 | policyPath string 31 | 32 | // policy is the contents of the policy file to apply 33 | policy string 34 | 35 | // Show status too? 36 | status bool 37 | 38 | // Show all stacks, even deleted ones? 39 | all bool 40 | } 41 | 42 | // Arguments adds per-command args to the object. 43 | func (sc *stacksCommand) Arguments(f *flag.FlagSet) { 44 | f.StringVar(&sc.rolesPath, "roles", "", "Path to a list of roles to process, one by one") 45 | f.StringVar(&sc.filter, "filter", "", "Show only stacks matching this filter") 46 | f.StringVar(&sc.policyPath, "policy", "", "Path to a stack-policy to apply to all stacks") 47 | f.BoolVar(&sc.status, "status", false, "Show the stack-status as well as the name?") 48 | f.BoolVar(&sc.all, "all", false, "Show even deleted stacks?") 49 | } 50 | 51 | // Info returns the name of this subcommand. 52 | func (sc *stacksCommand) Info() (string, string) { 53 | return "stacks", `List all cloudformation stack-names 54 | 55 | Details: 56 | 57 | This command allows you to list the names of all cloudformation stacks. 58 | 59 | Listing stacks via the AWS CLI is otherwise a bit annoying, and here we 60 | take care of excluding deleted stacks by default. This makes it simpler 61 | to use for scripting, and removes the necessity to have 'jq' available. 62 | 63 | Once way to use this is to apply a stack policy to all stacks, that 64 | can be done via the '-policy' argument. 65 | ` 66 | 67 | } 68 | 69 | // Execute is invoked if the user specifies this sub-command. 70 | func (sc *stacksCommand) Execute(args []string) int { 71 | 72 | // 73 | // If we have a policy-path then read it into our policy 74 | // 75 | if sc.policyPath != "" { 76 | content, err := os.ReadFile(sc.policyPath) 77 | if err != nil { 78 | fmt.Printf("error reading %s: %s\n", sc.policyPath, err) 79 | return 1 80 | } 81 | 82 | sc.policy = string(content) 83 | } 84 | 85 | // 86 | // Get the connection, using default credentials 87 | // 88 | session, err := utils.NewSession() 89 | if err != nil { 90 | fmt.Printf("%s\n", err.Error()) 91 | return 1 92 | } 93 | 94 | // 95 | // Now invoke our callback - this will call the function 96 | // "DisplayStacks" once if we're not running with a role-file, 97 | // otherwise once for each role. 98 | // 99 | errs := utils.HandleRoles(session, sc.rolesPath, sc.DisplayStacks, nil) 100 | 101 | if len(errs) > 0 { 102 | fmt.Printf("errors running display\n") 103 | 104 | for _, err := range errs { 105 | fmt.Printf("%s\n", err) 106 | } 107 | return 1 108 | } 109 | 110 | return 0 111 | } 112 | 113 | // Display is our callback method, which is invoked once for our main 114 | // account - if no roles-file is specified - or once for each assumed 115 | // role within that file. 116 | func (sc *stacksCommand) DisplayStacks(svc *ec2.EC2, account string, void interface{}) error { 117 | 118 | // Setup a session 119 | sess := session.Must(session.NewSessionWithOptions(session.Options{ 120 | SharedConfigState: session.SharedConfigEnable, 121 | })) 122 | 123 | // Get the cloudformation service 124 | cf := cloudformation.New(sess) 125 | input := &cloudformation.ListStacksInput{StackStatusFilter: []*string{}} 126 | 127 | // List the stacks 128 | resp, err := cf.ListStacks(input) 129 | if err != nil { 130 | return err 131 | } 132 | 133 | // Create a map for recording name => [status1, status2..]. 134 | // 135 | // We do this primarily because we want to show the 136 | // stack-names in sorted-order. 137 | lookup := make(map[string][]string) 138 | 139 | // Get all the stacks, and save their names/statuses in 140 | // a lookup table. 141 | for _, ent := range resp.StackSummaries { 142 | 143 | // Get the nam/status 144 | name := *ent.StackName 145 | status := *ent.StackStatus 146 | 147 | // Append the status to the name. 148 | // 149 | // This is necessary because the same stack-name might 150 | // be present multiple times, in different states: 151 | // 152 | // [DELETE_COMPLETE, DELETE_COMPLETE, UPDATE_COMPLETE] 153 | // 154 | lookup[name] = append(lookup[name], status) 155 | } 156 | 157 | // Sort the stack-names. 158 | keys := make([]string, 0, len(lookup)) 159 | for key := range lookup { 160 | keys = append(keys, key) 161 | } 162 | sort.Strings(keys) 163 | 164 | // Now we have a sorted list of stack-names we can iterate over them 165 | for _, key := range keys { 166 | 167 | // The stack-statuses comes from the lookup-map. 168 | val := lookup[key] 169 | 170 | // A stack might appear multiple times, in different 171 | // states: 172 | // 173 | // DELETE_COMPLETE, DELETE_COMPLETE, UPDATE_COMPLETE 174 | show := false 175 | 176 | // Don't show if "DELETE_COMPLETE" 177 | for _, state := range val { 178 | if !strings.Contains(state, "DELETE") { 179 | show = true 180 | } 181 | } 182 | 183 | // Should we filter this stack out? 184 | if sc.filter != "" { 185 | // If it doesn't match then skip it. 186 | match, er := regexp.MatchString(sc.filter, key) 187 | if er != nil { 188 | return fmt.Errorf("error running regexp match of %s against %s: %s", sc.filter, key, er) 189 | } 190 | if !match { 191 | show = false 192 | } 193 | } 194 | 195 | // Are we showing all? 196 | if !sc.all && !show { 197 | continue 198 | } 199 | 200 | // Show the name of the stack 201 | fmt.Printf("%s", key) 202 | 203 | // If `-status` show the status too 204 | if sc.status { 205 | fmt.Printf(" [%s]", strings.Join(val, ",")) 206 | } 207 | 208 | // Applying a policy? 209 | if sc.policy != "" { 210 | 211 | // Create the parameters 212 | params := &cloudformation.SetStackPolicyInput{ 213 | StackName: aws.String(key), 214 | StackPolicyBody: aws.String(sc.policy), 215 | } 216 | 217 | // Set the policy 218 | resp, err := cf.SetStackPolicy(params) 219 | if err != nil { 220 | fmt.Printf("error calling SetStackPolicy %s\n", err) 221 | return err 222 | } 223 | 224 | // Show the response 225 | fmt.Printf("SetStackPolicy(%s) -> %s\n", key, resp) 226 | } 227 | 228 | // Newline 229 | fmt.Printf("\n") 230 | } 231 | 232 | return nil 233 | } 234 | -------------------------------------------------------------------------------- /instances/instances.go: -------------------------------------------------------------------------------- 1 | // Package instances contains the common code which will find 2 | // running EC2 instances, and return details about them. 3 | package instances 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | 9 | "github.com/aws/aws-sdk-go/aws" 10 | "github.com/aws/aws-sdk-go/service/ec2" 11 | "github.com/skx/aws-utils/amiage" 12 | "github.com/skx/aws-utils/tag2name" 13 | ) 14 | 15 | // Volume holds detailed regarding an instances volumes. 16 | // 17 | // This structure is used to populate the text/template we use for output 18 | // generation. 19 | type Volume struct { 20 | // Device is the name of the device 21 | Device string 22 | 23 | // ID is the name of the ID 24 | ID string 25 | 26 | // Size is the size of the device. 27 | Size string 28 | 29 | // Type is the storage type. 30 | Type string 31 | 32 | // Encrypted contains the encryption value of the volume 33 | Encrypted string 34 | 35 | // IOPS holds the speed of the device. 36 | IOPS string 37 | } 38 | 39 | // InstanceOutput is the structure used to populate our templated output 40 | // 41 | // This structure is used to populate the text/template we use for output 42 | // generation. 43 | type InstanceOutput struct { 44 | 45 | // AWSAccount is the account number we're running under 46 | AWSAccount string 47 | 48 | // AvailabilityZone is the zone in which this instance is running 49 | AvailabilityZone string 50 | 51 | // InstanceID holds the AWS instance ID 52 | InstanceID string 53 | 54 | // InstanceName holds the AWS instance name, if set 55 | InstanceName string 56 | 57 | // InstanceAMI holds the AMI name 58 | InstanceAMI string 59 | 60 | // AMIAge contains the age of the AMI in days. 61 | AMIAge int 62 | 63 | // InstanceState holds the instance state (stopped, running, etc) 64 | InstanceState string 65 | 66 | // InstanceType holds the instance type "t2.tiny", etc. 67 | InstanceType string 68 | 69 | // Keypair setup for access. 70 | SSHKeyName string 71 | 72 | // SubnetID is the ID of the subnet the instance is running within. 73 | SubnetID string 74 | 75 | // PublicIPv4 has the public IPv4 address 76 | PublicIPv4 string 77 | 78 | // PrivateIPv4 has the private IPv4 address 79 | PrivateIPv4 string 80 | 81 | // Volumes holds all known volumes 82 | Volumes []Volume 83 | 84 | // VPCID is the ID of the VPC the instance is running within. 85 | VPCID string 86 | } 87 | 88 | // GetInstances returns details about our running instances. 89 | func GetInstances(svc *ec2.EC2, acct string) ([]InstanceOutput, error) { 90 | 91 | // Our return value 92 | ret := []InstanceOutput{} 93 | 94 | // Get the instances which are running/pending 95 | params := &ec2.DescribeInstancesInput{ 96 | Filters: []*ec2.Filter{ 97 | { 98 | Name: aws.String("instance-state-name"), 99 | Values: []*string{aws.String("running"), aws.String("pending")}, 100 | }, 101 | }, 102 | } 103 | 104 | // Create new EC2 client 105 | result, err := svc.DescribeInstances(params) 106 | if err != nil { 107 | return ret, fmt.Errorf("DescribeInstances failed: %s", err) 108 | } 109 | 110 | // For each instance build up an object to describe it 111 | for _, reservation := range result.Reservations { 112 | 113 | // The structure to output for this instance 114 | var out InstanceOutput 115 | 116 | for _, instance := range reservation.Instances { 117 | 118 | // We have a running EC2 instance, we'll populate 119 | // the InstanceOutput structure with details. 120 | 121 | // Values which are always present. 122 | out.AWSAccount = acct 123 | out.AvailabilityZone = *instance.Placement.AvailabilityZone 124 | out.SubnetID = *instance.SubnetId 125 | out.VPCID = *instance.VpcId 126 | out.InstanceID = *instance.InstanceId 127 | out.InstanceName = *instance.InstanceId 128 | out.InstanceState = *instance.State.Name 129 | out.InstanceType = *instance.InstanceType 130 | out.InstanceAMI = *instance.ImageId 131 | 132 | // Get the AMI age, in days. 133 | out.AMIAge, err = amiage.AMIAge(svc, out.InstanceAMI) 134 | if err != nil { 135 | if !errors.Is(err, amiage.NotFound) { 136 | return ret, fmt.Errorf("error getting AMI age for %s: %s", out.InstanceAMI, err) 137 | } 138 | } 139 | 140 | // Look for the name, which is set via a Tag. 141 | // 142 | // Default back to the InstanceID if no name was set. 143 | out.InstanceName = tag2name.Lookup(instance.Tags, *instance.InstanceId) 144 | 145 | // Optional values 146 | if instance.KeyName != nil { 147 | out.SSHKeyName = *instance.KeyName 148 | } 149 | if instance.PublicIpAddress != nil { 150 | out.PublicIPv4 = *instance.PublicIpAddress 151 | } 152 | if instance.PrivateIpAddress != nil { 153 | out.PrivateIPv4 = *instance.PrivateIpAddress 154 | } 155 | 156 | // Now the storage associated with the instance 157 | vols, err := readBlockDevicesFromInstance(instance, svc) 158 | if err == nil { 159 | for _, x := range vols["ebs"].([]map[string]interface{}) { 160 | 161 | out.Volumes = append(out.Volumes, Volume{ 162 | Device: fmt.Sprintf("%s", x["device_name"]), 163 | ID: fmt.Sprintf("%s", x["id"]), 164 | Size: fmt.Sprintf("%d", x["volume_size"]), 165 | Type: fmt.Sprintf("%s", x["volume_type"]), 166 | Encrypted: fmt.Sprintf("%t", x["encrypted"]), 167 | IOPS: fmt.Sprintf("%d", x["iops"])}) 168 | } 169 | } else { 170 | return ret, fmt.Errorf("failed to read devices %s", err) 171 | } 172 | 173 | ret = append(ret, out) 174 | } 175 | } 176 | 177 | return ret, nil 178 | } 179 | 180 | func readBlockDevicesFromInstance(instance *ec2.Instance, conn *ec2.EC2) (map[string]interface{}, error) { 181 | blockDevices := make(map[string]interface{}) 182 | blockDevices["ebs"] = make([]map[string]interface{}, 0) 183 | 184 | instanceBlockDevices := make(map[string]*ec2.InstanceBlockDeviceMapping) 185 | for _, bd := range instance.BlockDeviceMappings { 186 | if bd.Ebs != nil { 187 | instanceBlockDevices[*bd.Ebs.VolumeId] = bd 188 | } 189 | } 190 | 191 | if len(instanceBlockDevices) == 0 { 192 | return nil, nil 193 | } 194 | 195 | volIDs := make([]*string, 0, len(instanceBlockDevices)) 196 | for volID := range instanceBlockDevices { 197 | volIDs = append(volIDs, aws.String(volID)) 198 | } 199 | 200 | // Need to call DescribeVolumes to get volume_size and volume_type for each 201 | // EBS block device 202 | volResp, err := conn.DescribeVolumes(&ec2.DescribeVolumesInput{ 203 | VolumeIds: volIDs, 204 | }) 205 | if err != nil { 206 | return nil, err 207 | } 208 | 209 | for _, vol := range volResp.Volumes { 210 | instanceBd := instanceBlockDevices[*vol.VolumeId] 211 | bd := make(map[string]interface{}) 212 | 213 | bd["id"] = *vol.VolumeId 214 | if instanceBd.Ebs != nil && instanceBd.Ebs.DeleteOnTermination != nil { 215 | bd["delete_on_termination"] = *instanceBd.Ebs.DeleteOnTermination 216 | } 217 | if vol.Size != nil { 218 | bd["volume_size"] = *vol.Size 219 | } 220 | if vol.VolumeType != nil { 221 | bd["volume_type"] = *vol.VolumeType 222 | } 223 | if vol.Iops != nil { 224 | bd["iops"] = *vol.Iops 225 | } 226 | 227 | if instanceBd.DeviceName != nil { 228 | bd["device_name"] = *instanceBd.DeviceName 229 | } 230 | if vol.Encrypted != nil { 231 | bd["encrypted"] = *vol.Encrypted 232 | } 233 | if vol.SnapshotId != nil { 234 | bd["snapshot_id"] = *vol.SnapshotId 235 | } 236 | 237 | blockDevices["ebs"] = append(blockDevices["ebs"].([]map[string]interface{}), bd) 238 | } 239 | 240 | return blockDevices, nil 241 | } 242 | -------------------------------------------------------------------------------- /cmd_rotate_keys.go: -------------------------------------------------------------------------------- 1 | // Rotate the AWS access-keys, and update ~/.aws/credentials with the 2 | // new details. 3 | // 4 | // Only complication here is that we're limited by the number of keys 5 | // we have. 6 | 7 | package main 8 | 9 | import ( 10 | "bufio" 11 | "flag" 12 | "fmt" 13 | "os" 14 | "os/user" 15 | "path/filepath" 16 | "strings" 17 | 18 | "github.com/skx/aws-utils/utils" 19 | 20 | "github.com/aws/aws-sdk-go/aws/session" 21 | "github.com/aws/aws-sdk-go/service/iam" 22 | ) 23 | 24 | // Structure for our options and state. 25 | type rotateKeysCommand struct { 26 | 27 | // Should we force deletion of old keys without prompting? 28 | Force bool 29 | 30 | // Should we explicitly remove orphaned keys 31 | Cleanup bool 32 | 33 | // Configuration path, defaults to ~/.aws/credentials 34 | Path string 35 | } 36 | 37 | // Arguments adds per-command args to the object. 38 | func (r *rotateKeysCommand) Arguments(f *flag.FlagSet) { 39 | 40 | f.BoolVar(&r.Cleanup, "cleanup", false, "Should we remove orphaned keys, automatically?") 41 | f.BoolVar(&r.Force, "force", false, "Should we force removal of old keys without prompting?") 42 | f.StringVar(&r.Path, "path", "", "The location of the configuration file to modify?") 43 | 44 | } 45 | 46 | // Info returns the name of this subcommand. 47 | func (r *rotateKeysCommand) Info() (string, string) { 48 | return "rotate-keys", `Rotate your AWS access keys. 49 | 50 | Details: 51 | 52 | This command will attempt to generate a new set of AWS access keys, updating 53 | ~/.aws/credentials with the new details. 54 | 55 | AWS will only allow you to generate two sets of access-credentials, so before 56 | we generate a new public/private key we'll retrieved the existing set. If you 57 | have two sets already an existing entry must be removed. 58 | 59 | Rather than blindly removing an existing set of credentials you will be 60 | asked to confirm via an interactive prompt - unless you add '-force' to 61 | your command-line. 62 | 63 | NOTE: 64 | 65 | This sub-cmmand has only been tested with essentially "empty" credentials 66 | file which looks like so: 67 | 68 | [default] 69 | aws_access_key_id=XFDKLFJDSLFDSF 70 | aws_secret_access_key=3w39r8w0e9r8we09r8ewr90we8r09ew 71 | 72 | If you've got a more complex setup I'd urge you to take a backup before you 73 | execute this tool for the first time. 74 | ` 75 | 76 | } 77 | 78 | func (r *rotateKeysCommand) confirm() error { 79 | 80 | // Warning 81 | fmt.Printf("%s", colorRed) 82 | fmt.Printf("You already have 2 access keys in-use, we cannot generate more.\n") 83 | fmt.Printf("\n") 84 | fmt.Printf("Press Ctrl-C to cancel, or enter 'OK' (uppercase) to delete the oldest key.\n") 85 | fmt.Printf("\n") 86 | fmt.Printf("%s", colorReset) 87 | 88 | // Read a line of input 89 | reader := bufio.NewReader(os.Stdin) 90 | input, err := reader.ReadString('\n') 91 | if err != nil { 92 | return fmt.Errorf("failed to read from STDIN %s", err) 93 | } 94 | 95 | // Strip CR/newline from string, then leading/trailing spaces 96 | input = strings.TrimSuffix(input, "\n") 97 | input = strings.TrimSuffix(input, "\r") 98 | input = strings.TrimSpace(input) 99 | 100 | // no input 101 | if input == "" { 102 | return fmt.Errorf("aborting as you hit enter") 103 | } 104 | 105 | // not "OK" 106 | if input != "OK" { 107 | return fmt.Errorf("aborting as you entered '%s', not 'OK'", input) 108 | } 109 | 110 | // OK the user confirmed 111 | return nil 112 | } 113 | 114 | // setupPath populates the default path to the configuration file. 115 | // 116 | // 117 | func (r *rotateKeysCommand) setupPath() error { 118 | 119 | // If the user specified a path, use it. 120 | if r.Path != "" { 121 | return nil 122 | } 123 | 124 | // Is there an environmental variable? 125 | path := os.Getenv("AWS_SHARED_CREDENTIALS_FILE") 126 | if path != "" { 127 | r.Path = path 128 | return nil 129 | } 130 | 131 | // Otherwise infer via the current user 132 | usr, err := user.Current() 133 | if err != nil { 134 | return fmt.Errorf("failed to find current user: %s", err) 135 | } 136 | 137 | // Get the home-directory 138 | dir := usr.HomeDir 139 | 140 | // Join the path 141 | r.Path = filepath.Join(dir, ".aws", "credentials") 142 | return nil 143 | } 144 | 145 | // Execute is invoked if the user specifies this sub-command. 146 | func (r *rotateKeysCommand) Execute(args []string) int { 147 | 148 | // Discover a sane path to the credentials file 149 | err := r.setupPath() 150 | if err != nil { 151 | fmt.Printf("error finding credential path %s\n", err) 152 | return 1 153 | } 154 | 155 | // Start an AWS session 156 | var sess *session.Session 157 | sess, err = utils.NewSession() 158 | if err != nil { 159 | fmt.Printf("%s\n", err.Error()) 160 | return 1 161 | } 162 | 163 | // Create the handle and list keys - so we can see if we need to 164 | // remove an existing key before generating a fresh one. 165 | iamClient := iam.New(sess) 166 | keys, err := iamClient.ListAccessKeys(&iam.ListAccessKeysInput{}) 167 | if err != nil { 168 | fmt.Printf("error listing current keys: %s", err) 169 | return 1 170 | } 171 | 172 | // Keep track of any deleted keys here. 173 | deleted := []string{} 174 | 175 | // AWS only allows two keys, at the most. 176 | // 177 | // If there is one key then we just create a new one, meaning 178 | // we have hit the limit - and that's OK. 179 | // 180 | // If there were previously two keys already present then we 181 | // have to remove one before we can create the new replacement// 182 | // 183 | // Look for more than one? 184 | if len(keys.AccessKeyMetadata) > 1 { 185 | 186 | // If we're not forcing.. 187 | if !r.Force { 188 | 189 | // Then ensure the user confirms removal. 190 | err = r.confirm() 191 | if err != nil { 192 | fmt.Printf("%s\n", err) 193 | return 1 194 | } 195 | } 196 | 197 | // Remove the older key. 198 | _, err = iamClient.DeleteAccessKey(&iam.DeleteAccessKeyInput{ 199 | AccessKeyId: keys.AccessKeyMetadata[0].AccessKeyId, 200 | }) 201 | 202 | // Abort on error 203 | if err != nil { 204 | fmt.Printf("error deleting oldest key: %s\n", err) 205 | return 1 206 | } 207 | 208 | deleted = append(deleted, *keys.AccessKeyMetadata[0].AccessKeyId) 209 | 210 | // At this point we've changed: 211 | // 212 | // There were two keys - the maximum. 213 | // 214 | // We've now removed one, which means we have room to create 215 | // a new one. 216 | } 217 | 218 | // Open the existing credentials file 219 | file, err := os.Open(r.Path) 220 | if err != nil { 221 | fmt.Printf("couldn't open file %s for reading: %s\n", r.Path, err) 222 | return 1 223 | } 224 | defer file.Close() 225 | 226 | // Actually create the new key now. 227 | created, err := iamClient.CreateAccessKey(&iam.CreateAccessKeyInput{}) 228 | if err != nil { 229 | fmt.Printf("error creating the new key: %s\n", err) 230 | return 1 231 | } 232 | 233 | // Collect the lines from within the existing credentials file 234 | content := []string{} 235 | scanner := bufio.NewScanner(file) 236 | for scanner.Scan() { 237 | content = append(content, scanner.Text()) 238 | } 239 | if err = scanner.Err(); err != nil { 240 | fmt.Printf("error processing the config file: %s\n", err) 241 | return 1 242 | } 243 | 244 | // Now create a new file and output our updated values there 245 | out, err2 := os.Create(r.Path) 246 | if err2 != nil { 247 | fmt.Printf("created new keys, but couldn't open output file %s: %s\n", r.Path, err2) 248 | return 1 249 | } 250 | 251 | awsAccessKeyID := false 252 | awsSecretAccessKeyID := false 253 | 254 | // Rewrite here. 255 | for _, line := range content { 256 | 257 | // Update in-place 258 | if !awsAccessKeyID && strings.HasPrefix(line, "aws_access_key_id") { 259 | 260 | // only update the first one 261 | _, err = out.WriteString("aws_access_key_id=" + *created.AccessKey.AccessKeyId + "\n") 262 | if err != nil { 263 | fmt.Printf("error writing to file:%s\n", err.Error()) 264 | return 1 265 | } 266 | awsAccessKeyID = true 267 | continue 268 | } 269 | 270 | // Update in-place 271 | if !awsSecretAccessKeyID && strings.HasPrefix(line, "aws_secret_access_key") { 272 | _, err = out.WriteString("aws_secret_access_key=" + *created.AccessKey.SecretAccessKey + "\n") 273 | if err != nil { 274 | fmt.Printf("error writing to file:%s\n", err.Error()) 275 | return 1 276 | } 277 | 278 | awsSecretAccessKeyID = true 279 | continue 280 | } 281 | 282 | // Otherwise copy the old line into place. 283 | _, err = out.WriteString(line + "\n") 284 | if err != nil { 285 | fmt.Printf("error writing to file:%s\n", err.Error()) 286 | return 1 287 | } 288 | 289 | } 290 | 291 | // Close the output file 292 | out.Close() 293 | 294 | // At this point we've created a new key, and handled the 295 | // update of the users configuration file. 296 | // 297 | // If we're going to clean any orphaned keys we should do that now. 298 | if r.Cleanup { 299 | 300 | // We previous retrieved the keys when we started, 301 | // so loop over those. 302 | // 303 | // Any key that is present there should be removed. 304 | for _, key := range(keys.AccessKeyMetadata) { 305 | 306 | fmt.Printf("Removing orphaned key: %s", *key.AccessKeyId) 307 | 308 | // Did we already delete this key? 309 | removed := false 310 | 311 | for _, cur := range deleted { 312 | if cur == *key.AccessKeyId { 313 | removed = true 314 | } 315 | } 316 | 317 | // Delete the key, if not previously done. 318 | if removed { 319 | fmt.Printf(" - Already removed as part of rotation") 320 | } else { 321 | 322 | _, err = iamClient.DeleteAccessKey(&iam.DeleteAccessKeyInput{ 323 | AccessKeyId: key.AccessKeyId, 324 | }) 325 | 326 | // Failure to delete the key is unfortunate, 327 | // but it isn't terminal. 328 | // 329 | // We have after all already created a new 330 | // key and updated the users' config to use it. 331 | if err != nil { 332 | fmt.Printf(" - Failure: %s", err.Error()) 333 | } 334 | } 335 | 336 | // newline at the end of the output. 337 | fmt.Printf("\n") 338 | } 339 | } 340 | 341 | return 0 342 | } 343 | -------------------------------------------------------------------------------- /cmd_csv_instances.go: -------------------------------------------------------------------------------- 1 | // Show summary of instance details, as CSV. 2 | // 3 | // Primarily written to find instances running with old AMIs 4 | 5 | package main 6 | 7 | import ( 8 | "bytes" 9 | "flag" 10 | "fmt" 11 | "regexp" 12 | "strings" 13 | 14 | "github.com/skx/aws-utils/instances" 15 | "github.com/skx/aws-utils/tag2name" 16 | "github.com/skx/aws-utils/utils" 17 | 18 | "github.com/aws/aws-sdk-go/aws/awserr" 19 | "github.com/aws/aws-sdk-go/service/ec2" 20 | ) 21 | 22 | // Structure for our options and state. 23 | type csvInstancesCommand struct { 24 | 25 | // Path to a file containing roles 26 | rolesPath string 27 | 28 | // Have we shown the CSV header? 29 | header bool 30 | 31 | // Format string to print 32 | format string 33 | 34 | // Filter to show only matching lines 35 | filter string 36 | } 37 | 38 | // Arguments adds per-command args to the object. 39 | func (c *csvInstancesCommand) Arguments(f *flag.FlagSet) { 40 | f.StringVar(&c.rolesPath, "roles", "", "Path to a list of roles to process, one by one") 41 | f.StringVar(&c.format, "format", "", "Format string of the fields to print") 42 | f.StringVar(&c.filter, "filter", "", "Only show lines matching this regular expression") 43 | } 44 | 45 | // Info returns the name of this subcommand. 46 | func (c *csvInstancesCommand) Info() (string, string) { 47 | return "csv-instances", `Export a summary of running instances. 48 | 49 | Details: 50 | 51 | This command exports a list of the running instances which are available 52 | to the logged in account, in CSV format. 53 | 54 | By default the export contains the following fields: 55 | 56 | * Account ID 57 | * Instance ID 58 | * Instance Name 59 | * AMI ID 60 | 61 | You can specify a different output via the 'format' argument, for 62 | example: 63 | 64 | aws-utils csv-instances --format="account,id,name,ipv4address" 65 | 66 | Valid fields are 67 | 68 | * "account" - The AWS account-number. 69 | * "az" - The availability zone within which the instance is running. 70 | * "ami" - The AMI name of the running instance. 71 | * "amiage" - The age of the AMI in days. 72 | * "id" - The instance ID. 73 | * "name" - The instance name, as set via tags. 74 | * "privateipv4" - The (private) IPv4 address associated with the instance. 75 | * "publicipv4" - The (public) IPv4 address associated with the instance. 76 | * "ssh-key" - The SSH key setup for this instance. 77 | * "state" - The instance state (running, pending, etc). 78 | * "subnet" - The name of the subnet within which the instance is running. 79 | * "subnetid" - The ID of the subnet within which the instance is running. 80 | * "type" - The instance type (t2.small, t3.large, etc). 81 | * "vpc" - The name of the VPC within which the instance is running. 82 | * "vpcid" - The ID of the VPC within which the instance is running. 83 | ` 84 | 85 | } 86 | 87 | // DumpCSV outputs the list of running instances. 88 | func (c *csvInstancesCommand) DumpCSV(svc *ec2.EC2, acct string, void interface{}) error { 89 | 90 | // Get the running instances. 91 | ret, err := instances.GetInstances(svc, acct) 92 | if err != nil { 93 | return err 94 | } 95 | 96 | // Get the format-string 97 | format := c.format 98 | if format == "" { 99 | format = "account,id,name,ami" 100 | } 101 | 102 | // Map of subnet names to IDs. 103 | var subnets map[string]string 104 | fetchSubnets := false 105 | 106 | // Map of VPC names to IDs 107 | var vpcs map[string]string 108 | fetchVPCs := false 109 | 110 | // Split the fields, by comma 111 | supplied := strings.Split(format, ",") 112 | 113 | // Ensure all fields are lower-cased and stripped of spaces 114 | fields := []string{} 115 | for _, field := range supplied { 116 | field = strings.TrimSpace(field) 117 | field = strings.ToLower(field) 118 | fields = append(fields, field) 119 | 120 | // Do we need to fetch subnet information? 121 | if field == "subnet" { 122 | fetchSubnets = true 123 | } 124 | if field == "vpc" { 125 | fetchVPCs = true 126 | } 127 | } 128 | 129 | // Fetch the subnets within the account, if we're going 130 | // to display the human-readable name. 131 | if fetchSubnets { 132 | 133 | // An empty filter, to get all subnets 134 | input := &ec2.DescribeSubnetsInput{ 135 | Filters: []*ec2.Filter{ 136 | {}, 137 | }, 138 | } 139 | 140 | // describe the subnets 141 | result, err := svc.DescribeSubnets(input) 142 | if err != nil { 143 | if aerr, ok := err.(awserr.Error); ok { 144 | switch aerr.Code() { 145 | default: 146 | fmt.Println(aerr.Error()) 147 | } 148 | } else { 149 | // Print the error, cast err to awserr.Error to get the Code and 150 | // Message from an error. 151 | fmt.Println(err.Error()) 152 | } 153 | return fmt.Errorf("failed to get subnets for account %s", acct) 154 | } 155 | 156 | // fill up our map 157 | subnets = make(map[string]string) 158 | 159 | // populate it with "id -> name" 160 | for i := range result.Subnets { 161 | // Get the name, via tags, if present 162 | name := tag2name.Lookup(result.Subnets[i].Tags, "unnamed") 163 | subnets[*result.Subnets[i].SubnetId] = name 164 | } 165 | } 166 | 167 | // Fetch the VPCs within the account, if we're going 168 | // to display the human-readable name. 169 | if fetchVPCs { 170 | 171 | // An empty filter, to get all subnets 172 | input := &ec2.DescribeVpcsInput{ 173 | Filters: []*ec2.Filter{ 174 | {}, 175 | }, 176 | } 177 | 178 | // describe the vpcs 179 | result, err := svc.DescribeVpcs(input) 180 | if err != nil { 181 | if aerr, ok := err.(awserr.Error); ok { 182 | switch aerr.Code() { 183 | default: 184 | fmt.Println(aerr.Error()) 185 | } 186 | } else { 187 | // Print the error, cast err to awserr.Error to get the Code and 188 | // Message from an error. 189 | fmt.Println(err.Error()) 190 | } 191 | return fmt.Errorf("failed to get VPCs for account %s", acct) 192 | } 193 | 194 | // fill up our map 195 | vpcs = make(map[string]string) 196 | 197 | // populate it with "id -> name" 198 | for i := range result.Vpcs { 199 | // Get the name, via tags, if present 200 | name := tag2name.Lookup(result.Vpcs[i].Tags, "unnamed") 201 | vpcs[*result.Vpcs[i].VpcId] = name 202 | } 203 | } 204 | 205 | // For each instance we've discovered 206 | for _, obj := range ret { 207 | 208 | // If we've not printed the header.. 209 | if !c.header { 210 | 211 | // Show something human-readable 212 | for i, field := range fields { 213 | 214 | switch field { 215 | case "account": 216 | fmt.Printf("Account ID") 217 | case "ami": 218 | fmt.Printf("AMI ID") 219 | case "amiage": 220 | fmt.Printf("AMI Age") 221 | case "az": 222 | fmt.Printf("Availability Zone") 223 | case "id": 224 | fmt.Printf("Instance ID") 225 | case "name": 226 | fmt.Printf("Name") 227 | case "privateipv4": 228 | fmt.Printf("PrivateIPv4") 229 | case "publicipv4": 230 | fmt.Printf("PublicIPv4") 231 | case "ssh-key": 232 | fmt.Printf("SSH Key") 233 | case "state": 234 | fmt.Printf("Instance State") 235 | case "subnet": 236 | fmt.Printf("Subnet") 237 | case "subnetid": 238 | fmt.Printf("Subnet ID") 239 | case "type": 240 | fmt.Printf("Instance Type") 241 | case "vpc": 242 | fmt.Printf("VPC") 243 | case "vpcid": 244 | fmt.Printf("VPC ID") 245 | default: 246 | fmt.Printf("unknown field:%s", field) 247 | } 248 | 249 | // if this isn't the last one, add "," 250 | if i < len(fields)-1 { 251 | fmt.Printf(",") 252 | } 253 | 254 | } 255 | 256 | // Terminate the header with a newline 257 | fmt.Printf("\n") 258 | c.header = true 259 | } 260 | 261 | // Buffer for this line of output 262 | var line bytes.Buffer 263 | 264 | // Show each field 265 | for i, field := range fields { 266 | 267 | switch field { 268 | case "account": 269 | line.WriteString(acct) 270 | case "ami": 271 | line.WriteString(obj.InstanceAMI) 272 | case "amiage": 273 | line.WriteString(fmt.Sprintf("%d", obj.AMIAge)) 274 | case "az": 275 | line.WriteString(obj.AvailabilityZone) 276 | case "id": 277 | line.WriteString(obj.InstanceID) 278 | case "name": 279 | line.WriteString(obj.InstanceName) 280 | case "privateipv4": 281 | line.WriteString(obj.PrivateIPv4) 282 | case "publicipv4": 283 | line.WriteString(obj.PublicIPv4) 284 | case "ssh-key": 285 | line.WriteString(obj.SSHKeyName) 286 | case "state": 287 | line.WriteString(obj.InstanceState) 288 | case "subnet": 289 | line.WriteString(subnets[obj.SubnetID]) 290 | case "subnetid": 291 | line.WriteString(obj.SubnetID) 292 | case "type": 293 | line.WriteString(obj.InstanceType) 294 | case "vpc": 295 | line.WriteString(vpcs[obj.VPCID]) 296 | case "vpcid": 297 | line.WriteString(obj.VPCID) 298 | default: 299 | fmt.Printf("unknown field:%s", field) 300 | } 301 | 302 | // if this isn't the last one, add "," 303 | if i < len(fields)-1 { 304 | line.WriteString(",") 305 | } 306 | } 307 | 308 | show := true 309 | 310 | // Should we filter this line out? 311 | if c.filter != "" { 312 | // If it doesn't match then skip it. 313 | match, er := regexp.MatchString(c.filter, line.String()) 314 | if er != nil { 315 | return fmt.Errorf("error running regexp match of %s against %s: %s", c.filter, line.String(), er) 316 | } 317 | if !match { 318 | show = false 319 | } 320 | } 321 | 322 | // Newline between records 323 | if show { 324 | fmt.Printf("%s\n", line.String()) 325 | } 326 | 327 | } 328 | return nil 329 | } 330 | 331 | // Execute is invoked if the user specifies this subcommand. 332 | func (c *csvInstancesCommand) Execute(args []string) int { 333 | 334 | // 335 | // Get the connection, using default credentials 336 | // 337 | session, err := utils.NewSession() 338 | if err != nil { 339 | fmt.Printf("%s\n", err.Error()) 340 | return 1 341 | } 342 | 343 | // 344 | // Now invoke our callback - this will call the function 345 | // "DumpCSV" once if we're not running with a role-file, 346 | // otherwise once for each role. 347 | // 348 | errs := utils.HandleRoles(session, c.rolesPath, c.DumpCSV, nil) 349 | if len(errs) > 0 { 350 | fmt.Printf("errors running CSV-Dump\n") 351 | for _, err := range errs { 352 | fmt.Printf("%s\n", err) 353 | } 354 | return 1 355 | } 356 | 357 | return 0 358 | } 359 | -------------------------------------------------------------------------------- /cmd_whitelist_self.go: -------------------------------------------------------------------------------- 1 | // This is just useful! 2 | 3 | package main 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "os" 11 | 12 | "github.com/skx/aws-utils/utils" 13 | "github.com/skx/subcommands" 14 | 15 | "github.com/aws/aws-sdk-go/aws" 16 | "github.com/aws/aws-sdk-go/aws/credentials/stscreds" 17 | "github.com/aws/aws-sdk-go/aws/session" 18 | "github.com/aws/aws-sdk-go/service/ec2" 19 | ) 20 | 21 | // ToChange contains the structure we're going to work with. 22 | type ToChange struct { 23 | // SG contains the security-group ID which should be updated. 24 | SG string 25 | 26 | // Name contains the description of the rule that we'll update. 27 | // Updating here means "removing with old IP" and "adding with new IP". 28 | Name string 29 | 30 | // Role contains the AWS role to assume before proceeding to look 31 | // at the security-group specified. 32 | Role string 33 | 34 | // The port which will be whitelisted (TCP-only). 35 | Port int 36 | 37 | // Display contains a message to display to the user. 38 | Display string 39 | } 40 | 41 | // Structure for our options and state. 42 | type whitelistSelfCommand struct { 43 | 44 | // IP contains the IP we've discovered. 45 | IP string 46 | 47 | // We embed the NoFlags option, because we accept no command-line flags. 48 | subcommands.NoFlags 49 | } 50 | 51 | // Info returns the name of this subcommand. 52 | func (i *whitelistSelfCommand) Info() (string, string) { 53 | return "whitelist-self", `Update security-groups with your external IP. 54 | 55 | Details: 56 | 57 | Assume you have some security-groups which contain allow-lists of single IPs. 58 | This command allows you to quickly and easily update those to keep your own 59 | entry current. 60 | 61 | You should provide a configuration file containing a series of rules, where 62 | each rule contains: 63 | 64 | * The security-group ID to which it applies. 65 | * The description to use for the rule. 66 | * This MUST be unique within the security-group. 67 | * Duplicates will be detected and will stop processing. 68 | * The port to open. 69 | * Optionally you may specify the ARN of an AWS role to assume before starting. 70 | 71 | For example the following would be a good input file: 72 | 73 | [ 74 | { 75 | "SG": "sg-12345", 76 | "Name": "[aws-utils] Steve home", 77 | "Port": 443 78 | }, 79 | { 80 | "SG": "sg-abcdef", 81 | "Name": "[aws-utils] Steve home", 82 | "Role": "arn:aws:iam::112233445566:role/devops-access-abcdef", 83 | "Port": 443 84 | } 85 | 86 | ] 87 | 88 | When executed this command will then iterate over the rules contained in 89 | the input-file. For each rule it will examine the specified security-group, 90 | removing any entry with the same name as you've specified, before re-adding 91 | it with your current external IP. 92 | 93 | While you may only specify a single port in a rule you can add multiple 94 | rules to cover the case where you want to whitelist two ports - for example: 95 | 96 | [ 97 | { 98 | "SG": "sg-12345", 99 | "Name": "[aws-utils] Steve home - HTTPS", 100 | "Port": 443 101 | }, 102 | { 103 | "SG": "sg-12345", 104 | "Name": "[aws-utils] Steve home - SSH", 105 | "Port": 22 106 | } 107 | ] 108 | 109 | NOTE: This only examines Ingress Rules, there are no changes made to Egress 110 | rules. 111 | 112 | To ease portability environmental variables are exported so you may write: 113 | 114 | "Name": "[aws-utils] - SSH - ${USER}", 115 | ` 116 | 117 | } 118 | 119 | // getIP returns the public IP address of the user, via the use of 120 | // the http://ip-api.com/ website. 121 | func (i *whitelistSelfCommand) getIP() (string, error) { 122 | 123 | type IP struct { 124 | Query string 125 | } 126 | 127 | // Make a HTTP-request 128 | req, err := http.Get("http://ip-api.com/json/") 129 | if err != nil { 130 | return "", err 131 | } 132 | defer req.Body.Close() 133 | 134 | // Read the body 135 | body, err := io.ReadAll(req.Body) 136 | if err != nil { 137 | return "", err 138 | } 139 | 140 | // Decode the result 141 | var ip IP 142 | err = json.Unmarshal(body, &ip) 143 | if err != nil { 144 | return "", err 145 | } 146 | 147 | // Add a "/32" to the IP as that is what we'll need to use 148 | // when we're looking at the security-groups. 149 | return ip.Query + "/32", nil 150 | } 151 | 152 | // processSG looks at the security-group for any entry with the given 153 | // description: 154 | // 155 | // 1. If no entries exist with that description it is added. 156 | // 157 | // 2. If multiple entries exist with that description report a fatal error. 158 | // 159 | // 3. If a single entry exists with the wrong IP, remove it and add the new 160 | // IP. Otherwise do nothing as the IP matches. 161 | func (i *whitelistSelfCommand) processSG(svc *ec2.EC2, groupid, desc string, port int64) error { 162 | 163 | // Get the contents of the security group. 164 | current, err := svc.DescribeSecurityGroups(&ec2.DescribeSecurityGroupsInput{ 165 | GroupIds: aws.StringSlice([]string{groupid}), 166 | }) 167 | if err != nil { 168 | return err 169 | } 170 | 171 | // Ensure that the description is unique before we do anything 172 | // destructive - count the number of rules that have the 173 | // specified text as the description. 174 | count := 0 175 | for _, sg := range current.SecurityGroups { 176 | for _, ipp := range sg.IpPermissions { 177 | for _, ipr := range ipp.IpRanges { 178 | if ipr.Description != nil && desc == *ipr.Description { 179 | count++ 180 | } 181 | } 182 | } 183 | } 184 | 185 | // If we found zero rules which have the specified description 186 | // we need to add the new entry. 187 | if count == 0 { 188 | 189 | // 190 | // Add the current IP to the whitelist 191 | // 192 | return i.myIPAdd(svc, groupid, desc, port) 193 | } 194 | 195 | // If we have more than rule which contains the description then 196 | // we must abort. 197 | if count > 1 { 198 | return fmt.Errorf("there are %d rules which have the description '%s' - aborting", count, desc) 199 | } 200 | 201 | // OK we have one rule which has the expected description. 202 | // 203 | // Do we need to change the IP? 204 | for _, sg := range current.SecurityGroups { 205 | 206 | // For each rule 207 | for _, ipp := range sg.IpPermissions { 208 | 209 | // for each CIDR range 210 | for _, ipr := range ipp.IpRanges { 211 | 212 | // Look for the description which is ours 213 | if ipr.Description != nil && desc == *ipr.Description { 214 | 215 | // If the IP is the same 216 | // then we do nothing 217 | if *ipr.CidrIp == i.IP { 218 | fmt.Printf(" Existing entry already matches current IP - no change\n") 219 | return nil 220 | } 221 | 222 | fmt.Printf(" REMOVING %s from security-group.\n", *ipr.CidrIp) 223 | err = i.myIPDel(svc, groupid, desc, ipr, port) 224 | if err != nil { 225 | return fmt.Errorf("error removing entry %s", err) 226 | } 227 | 228 | return i.myIPAdd(svc, groupid, desc, port) 229 | } 230 | } 231 | } 232 | } 233 | return nil 234 | } 235 | 236 | // myIPDel removes a CIDR range from the given security-group, with the 237 | // specified port. 238 | func (i *whitelistSelfCommand) myIPDel(svc *ec2.EC2, groupid, desc string, ipr *ec2.IpRange, port int64) error { 239 | // Otherwise we need to delete 240 | // the existing rule, and add 241 | // a new one. 242 | ipranges := []*ec2.IpRange{{ 243 | CidrIp: ipr.CidrIp, 244 | Description: aws.String(desc), 245 | }} 246 | 247 | // Delete the rule we've found 248 | _, err := svc.RevokeSecurityGroupIngress(&ec2.RevokeSecurityGroupIngressInput{ 249 | GroupId: aws.String(groupid), 250 | IpPermissions: []*ec2.IpPermission{{ 251 | IpProtocol: aws.String("tcp"), 252 | FromPort: aws.Int64(port), 253 | ToPort: aws.Int64(port), 254 | IpRanges: ipranges, 255 | }}, 256 | }) 257 | return err 258 | } 259 | 260 | // myIPAdd adds a new CIDR range to the given security-group, with the 261 | // specified port. 262 | func (i *whitelistSelfCommand) myIPAdd(svc *ec2.EC2, groupid, desc string, port int64) error { 263 | 264 | fmt.Printf(" ADDING %s to security-group.\n", i.IP) 265 | // Add the entry to the group 266 | var err error 267 | _, err = svc.AuthorizeSecurityGroupIngress(&ec2.AuthorizeSecurityGroupIngressInput{ 268 | GroupId: aws.String(groupid), 269 | IpPermissions: []*ec2.IpPermission{{ 270 | FromPort: aws.Int64(port), 271 | ToPort: aws.Int64(port), 272 | IpProtocol: aws.String("tcp"), 273 | IpRanges: []*ec2.IpRange{{ 274 | CidrIp: aws.String(i.IP), 275 | Description: aws.String(desc), 276 | }}, 277 | }}, 278 | }) 279 | 280 | return err 281 | } 282 | 283 | // handleSecurityGroup handles the application of the rule to one 284 | // security-group 285 | func (i *whitelistSelfCommand) handleSecurityGroup(entry ToChange, sess *session.Session) error { 286 | 287 | // Get a handle to the service to use. 288 | svc := ec2.New(sess) 289 | 290 | // If we have a role then use it. 291 | if entry.Role != "" { 292 | // process 293 | creds := stscreds.NewCredentials(sess, entry.Role) 294 | 295 | // Create service client value configured for credentials 296 | // from assumed role. 297 | svc = ec2.New(sess, &aws.Config{Credentials: creds}) 298 | } 299 | 300 | // No port specified? Then default to HTTPS. 301 | if entry.Port == 0 { 302 | entry.Port = 443 303 | } 304 | if entry.Name == "" { 305 | fmt.Printf("%s IGNORED rule with no Name field set.%s\n", colorRed, colorReset) 306 | return nil 307 | } 308 | 309 | fmt.Printf("\n") 310 | if entry.Role != "" { 311 | fmt.Printf(" Role: %s\n", entry.Role) 312 | } 313 | fmt.Printf(" SecurityGroupID: %s\n", entry.SG) 314 | fmt.Printf(" IP: %s\n", i.IP) 315 | fmt.Printf(" Port: %d\n", entry.Port) 316 | fmt.Printf(" Description: %s\n", entry.Name) 317 | 318 | if entry.Display != "" { 319 | fmt.Printf(" %s\n", entry.Display) 320 | } 321 | 322 | // Remove any existing rule with this name/description 323 | err := i.processSG(svc, entry.SG, entry.Name, int64(entry.Port)) 324 | if err != nil { 325 | return err 326 | } 327 | 328 | return nil 329 | } 330 | 331 | // processRules reads and processes rules contained within the specified 332 | // JSON file. 333 | func (i *whitelistSelfCommand) processRules(file string) error { 334 | 335 | // Read the file 336 | cnf, err := os.ReadFile(file) 337 | if err != nil { 338 | return fmt.Errorf("failed to read %s - %s", file, err) 339 | } 340 | 341 | // All the entries we know we're going to change, as read from 342 | // the input JSON file. 343 | var changes []ToChange 344 | 345 | // Parse our JSON into a list of rules. 346 | if err = json.Unmarshal(cnf, &changes); err != nil { 347 | return fmt.Errorf("error loading JSON: %s", err) 348 | } 349 | 350 | // Create a new AWS session 351 | sess, err := utils.NewSession() 352 | if err != nil { 353 | return fmt.Errorf("aws login failed: %s", err.Error()) 354 | } 355 | 356 | // Process each group 357 | for _, entry := range changes { 358 | 359 | // Expand any variables in the name first 360 | entry.Name = os.ExpandEnv(entry.Name) 361 | 362 | // Now handle the additional/removal 363 | err := i.handleSecurityGroup(entry, sess) 364 | if err != nil { 365 | return fmt.Errorf("error updating %s", err) 366 | } 367 | } 368 | 369 | return nil 370 | } 371 | 372 | // Execute is invoked if the user chooses this sub-command. 373 | func (i *whitelistSelfCommand) Execute(args []string) int { 374 | 375 | // Ensure we have a configuration file 376 | if len(args) < 1 { 377 | fmt.Printf("Usage: aws-utils whitelist-self config1.json config2.json .. configN.json\n") 378 | return 1 379 | } 380 | 381 | // Get our remote IP. 382 | ip, err := i.getIP() 383 | if err != nil { 384 | fmt.Printf("Error finding your public IP: %s\n", err) 385 | return 1 386 | } 387 | fmt.Printf("Your remote IP is %s\n", ip) 388 | 389 | // Save the current IP away 390 | i.IP = ip 391 | 392 | // For each filename on the command line 393 | for _, file := range args { 394 | 395 | // Process the file 396 | err = i.processRules(file) 397 | 398 | // Errors? Then show them, but continue if there are more files 399 | if err != nil { 400 | fmt.Printf("Error %s\n", err) 401 | } 402 | } 403 | 404 | return 0 405 | } 406 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Go Report Card](https://goreportcard.com/badge/github.com/skx/aws-utils)](https://goreportcard.com/report/github.com/skx/aws-utils) 2 | [![license](https://img.shields.io/github/license/skx/aws-utils.svg)](https://github.com/skx/aws-utils/blob/master/LICENSE) 3 | [![Release](https://img.shields.io/github/release/skx/aws-utils.svg)](https://github.com/skx/aws-utils/releases/latest) 4 | 5 | # AWS Utils 6 | 7 | This repository contains a simple CLI utility with a number of useful sub-commands for working with AWS, particularly for scripting and automation purposes. 8 | 9 | 10 | ## Motivation 11 | 12 | Several of the things that this tool does are possible via existing AWS utilities, such as the `aws-cli` package. However the difference in this tool is that it allows working across roles as a single command - so rather than running an export/list command 20+ times in the traditional way, you can run one command passing the list of roles to use, and all output will be created at once. 13 | 14 | Other sub-commands are just more useful, for example listing the available cloudformation stack-names with `aws-cli` will include deleted ones too, which need to be filtered out. Or allowing all stacks to be updated with a stack protection policy as a single command is just a time-saver. 15 | 16 | 17 | 18 | ## Installation 19 | 20 | If you the golang development tools installed upon your host, and you're running a recent version, you should be able to download and install via: 21 | 22 | ``` 23 | go install github.com/skx/aws-utils@latest 24 | ``` 25 | 26 | Or, after having cloned [this repository](https://github.com/skx/aws-utils) to your system, you can build from source with a simple: 27 | 28 | ``` 29 | go build . 30 | go install . 31 | ``` 32 | 33 | If you don't wish to build from source you should be able to find precompiled binaries for several operating systems upon our [releases page](https://github.com/skx/aws-utils/releases/) 34 | 35 | The binary contains embedded support for bash-completion, to enable this add the following to your bash startup-file: 36 | 37 | ``` 38 | source <(aws-utils bash-completion) 39 | ``` 40 | 41 | 42 | 43 | ## Help 44 | 45 | There is integrated help for each sub-command, for example running with no arguments will show you available commands: 46 | 47 | ```sh 48 | $ aws-utils 49 | Please specify a valid subcommand, choices are: 50 | 51 | bash-completion Generate and output a bash completion-script. 52 | commands Show all available sub-commands. 53 | csv-instances Export a summary of running instances. 54 | help Show usage information. 55 | ip Show the private IP of the given instance. 56 | instances Export a summary of running instances. 57 | orphaned-zones Show orphaned Route53 zones. 58 | rotate-keys Rotate your AWS access keys. 59 | sg-grep Security-Group Grep 60 | stacks List all cloudformation stack-names. 61 | subnets List subnets in all VPCs. 62 | version Show the version of this binary. 63 | whitelist-self Update security-groups with your external IP. 64 | whoami Show the current AWS user or role name. 65 | ``` 66 | 67 | Reading the help text, recommended, is down via the `help` sub-command: 68 | 69 | ``` 70 | $ aws-utils help whitelist-self 71 | 72 | Synopsis: 73 | Update security-groups with your external IP. 74 | 75 | Details: 76 | 77 | Assume you have some security-groups which contain allow-lists of single IPs. 78 | This command allows you to quickly and easily update those to keep your own 79 | entry current. 80 | 81 | ... 82 | ``` 83 | 84 | 85 | ## Common Features 86 | 87 | All of the commands accept the use of AWS credentials in the way you'd expect, be it from `~/.aws/credentials` or via the use of environmental-variables: 88 | 89 | * For authentication 90 | * `AWS_ACCESS_KEY_ID` 91 | * `AWS_SECRET_ACCESS_KEY` 92 | * `AWS_SESSION_TOKEN` (optionally) 93 | * `AWS_SHARED_CREDENTIALS_FILE` 94 | * The path to a credentials file (`~/.aws/credentials` by default). 95 | * Only used by the [rotate-keys](#rotate-keys) sub-command. 96 | * `AWS_REGION` 97 | * The region to use. 98 | 99 | These values are documented in the Golang SDK page: 100 | 101 | * https://docs.aws.amazon.com/sdk-for-go/api/aws/session/ 102 | 103 | Many of the utilities also allow you to operate upon an arbitrary number of AWS roles. In that case you'd specify the path to a file containing roles to assume, via the `-roles` argument. 104 | 105 | For example: 106 | 107 | ``` 108 | $ aws-utils csv-instances -roles=/path/to/roles 109 | ``` 110 | 111 | The format of the file is one-role per line, such as: 112 | 113 | ``` 114 | arn:aws:iam::123457000001:role/foo-AdministratorAccessFromInt-1ABCDEFGHIJKL 115 | arn:aws:iam::123457000002:role/foo-AdministratorAccessFromInt-2ABCDEFGHIJKL 116 | arn:aws:iam::123457000003:role/tst-AdministratorAccessFromInt-3ABCDEFGHIJKL 117 | arn:aws:iam::123457000004:role/tst-AdministratorAccessFromInt-4ABCDEFGHIJKL 118 | 119 | # Lines prefixed with "#" are comments, and are ignored (as are empty-lines). 120 | ``` 121 | 122 | 123 | 124 | ## SubCommands 125 | 126 | The following sub-commands are available: 127 | 128 | * [csv-instances](#csv-instances) 129 | * [instances](#instances) 130 | * [ip](#ip) 131 | * [orphaned-zones](#orphaned-zones) 132 | * [rotate-keys](#rotate-keys) 133 | * [sg-grep](#sg-grep) 134 | * [stacks](#stacks) 135 | * [subnets](#subnets) 136 | * [whitelist-self](#whitelist-self) 137 | * [whoami](#whoami) 138 | 139 | 140 | 141 | 142 | 143 | ### `csv-instances` 144 | 145 | Output a list of running instances, as CSV. The output may be changed, but by default we show: 146 | 147 | * Account ID 148 | * Instance ID 149 | * Instance Name 150 | * AMI ID 151 | 152 | Usage: 153 | 154 | ```sh 155 | $ aws-utils csv-instances [-roles=/path/to/roles] 156 | ``` 157 | 158 | The fields displayed may be changed via the `format` argument, for example: 159 | 160 | ```sh 161 | $ aws-utils csv-instances --format="name,id,subnet,subnetid,vpc,vpcid" 162 | ``` 163 | 164 | The list of available field-names can be viewed via `aws-utils help csv-instances`. 165 | 166 | 167 | ### `instances` 168 | 169 | Show a human-readable list of all the EC2 instances you have running, along 170 | with details of the volumes associated with each instance. 171 | 172 | Sample output: 173 | 174 | ``` 175 | i-01066633e12345567 - prod-fooapp-uk 176 | ------------------------------------ 177 | AMI : ami-01234567890abcdef 178 | AMI Age : 4 days 179 | Instance type: t3.medium 180 | Key name : sysadmin1 181 | Private IPv4 : 10.30.44.105 182 | Volumes: 183 | /dev/sda1 vol-01234567890abcdef 100Gb gp2 Encrypted:true IOPs:300 184 | ``` 185 | 186 | Usage: 187 | 188 | ```sh 189 | $ aws-utils instances [-json] [-roles=/path/to/roles] 190 | ``` 191 | 192 | The output is defined by a simple golang template. If you wish to change the template you can do so: 193 | 194 | ```sh 195 | $ aws-utils instances -dump-template > foo.tmpl 196 | $ vi foo.tmpl 197 | $ aws-utils instances -template=./foo.tmpl 198 | ``` 199 | 200 | 201 | 202 | ### `ip` 203 | 204 | Show the private IPv4 address of the instance which matches the given 205 | regular expression. 206 | 207 | ```sh 208 | $ aws-utils ip *live*manager 209 | 10.13.14.32 210 | ``` 211 | 212 | This sub-command is useful for tab-completion against instance names, for 213 | connecting via SSH/RDP/similar. 214 | 215 | 216 | 217 | ### `orphaned-zones` 218 | 219 | This sub-command examines all domains which have DNS hosted in Route53, 220 | and reports those which have nameservers configured which do __not__ 221 | belong to AWS. 222 | 223 | This is designed to identify domains which have expired, or had their 224 | DNS-hosting moved to an external system (such as cloudflare, or similar). 225 | 226 | Usage: 227 | 228 | ```sh 229 | $ aws-utils orphaned-zones 230 | VALID - dhcp.io. 231 | ORPHAN - example.com. 232 | ``` 233 | 234 | 235 | 236 | ### `rotate-keys` 237 | 238 | This sub-command uses the AWS API to regenerate a new set of AWS access-keys, 239 | and updates your `~/.aws/credentials` file with the new values. 240 | 241 | **NOTE**: 242 | 243 | * You may only have two sets of AWS Access Keys at a time 244 | * So if you have two already one must be removed. 245 | * You will be prompted prior to the removal of one, or you can add `-force` to avoid that interactive prompt. 246 | * `~/.aws/credentials` is the default file to use as the template for updating 247 | * If that file is missing your keys will be removed/created but they will then be lost. 248 | * This is because the output is achieved by reading the existing file and replacing existing keys, rather than blindly overwriting. 249 | * We want to do this to avoid data-loss on things like your profile(s) and other configuration values. 250 | * **Take a backup** before running this tool for the first time. 251 | 252 | 253 | 254 | ### `sg-grep` 255 | 256 | Show security-groups which match a particular regular expression. 257 | 258 | ``` 259 | $ aws-utils sg-grep 0.0.0.0/0 260 | sg-01234567890abcdef [eu-central-1] - launch-wizard-1 created 2021-11-19T09:39:15.473+02:00 261 | { 262 | Description: "launch-wizard-1 created 2021-11-19T09:39:15.473+02:00", 263 | GroupId: "sg-sg-01234567890abcdef", 264 | GroupName: "launch-wizard-1", 265 | IpPermissions: [{ 266 | FromPort: 22, 267 | IpProtocol: "tcp", 268 | IpRanges: [{ 269 | CidrIp: "0.0.0.0/0", 270 | Description: "" 271 | }], 272 | ToPort: 22 273 | }], 274 | 275 | ``` 276 | 277 | Usage: 278 | 279 | ```sh 280 | $ aws-utils sg-grep [-roles=/path/to/roles] search-term1 search-term2 .. 281 | ``` 282 | 283 | 284 | 285 | ### `stacks` 286 | 287 | Show the names, and optionally the statuses of all cloudformation stacks. 288 | 289 | This is useful for applying stack-policies to a list of stacks, for example, and avoids the use of more complex CLI invocations when using the AWS CLI. 290 | 291 | You can also update all stacks with a protection policy in a single operation. 292 | 293 | Usage: 294 | 295 | ```sh 296 | $ aws-utils stacks 297 | StackSet-09c62176-4401-4c2e-b018-b3983c37619d 298 | my-prod--iam 299 | my-prod--lifecycel-manager 300 | my-prod--route53 301 | .. 302 | 303 | $ aws-utils stacks -policy ./my-stack-policy.json 304 | ``` 305 | 306 | Optionally you may display the stack-status, and include deleted-stacks. 307 | 308 | 309 | 310 | 311 | ### `subnets` 312 | 313 | Show the subnets, along with associated CIDR ranges, available within all VPCs. 314 | 315 | This is useful when you're running a penetration test, or want a quick overview of all the available subnets across a bunch of accounts. 316 | 317 | Usage: 318 | 319 | ```sh 320 | $ aws-utils subnets 321 | Account, VPC, Subnet Name, Subnet ID, Cidr 322 | 207250808959,vpc-bbe705d2,default-eu-central-1a,subnet-b4df30dd,172.31.16.0/20 323 | 207250808959,vpc-bbe705d2,default-eu-central-1b,subnet-406c6238,172.31.0.0/20 324 | 207250808959,vpc-bbe705d2,default-eu-central-1a,subnet-44fad80e,172.31.32.0/20 325 | ``` 326 | 327 | 328 | 329 | 330 | ### `whitelist-self` 331 | 332 | This sub-command allows you to quickly update Ingress rules, with your current external IP address. 333 | 334 | Imagine you have a number of security-groups which permit access to resources via a small list of permitted source IPs this command will let you update your own entry in that list easily. 335 | 336 | Sample input file: 337 | 338 | ``` 339 | $ cat input.json 340 | [ 341 | { "SG": "sg-12344", "Name": "[aws-utils] Steve's Home IP", "Port": 443 }, 342 | { "SG": "sg-12345", "Name": "[aws-utils] Steve's Home IP", "Port": 22 } 343 | ] 344 | ``` 345 | 346 | Valid values for the JSON object are: 347 | 348 | * `Display` 349 | * A message to display when the group is updated, good for documentation. 350 | * `SG` 351 | * The ID of the security-group to update. 352 | * `Name` 353 | * The name of the rule to add (i.e. description) 354 | * This **must** be unique within the security-group. 355 | * `Port` 356 | * The port to permit. 357 | * `Role` 358 | * Optionally you may specify an ARN of a role to assume. 359 | * example : `arn:aws:iam::123456789010:role/devops-access` 360 | 361 | 362 | As you can see each rule allows you to whitelist a single port, and only a single port. Of course if you wish you can repeat rules with different ports like so: 363 | 364 | ```json 365 | [ 366 | { 367 | "SG": "sg-12345", 368 | "Name": "[aws-utils] ${USER} home: HTTPS", 369 | "Port": 443 370 | }, 371 | { 372 | "SG": "sg-12345", 373 | "Name": "[aws-utils] ${USER} home: SSH", 374 | "Port": 22 375 | } 376 | ] 377 | ``` 378 | 379 | Once you run the tool, with a suitable JSON input file, you'll get output like so: 380 | 381 | ``` 382 | $ ./aws-utils whitelist-self ./prod.json 383 | Your remote IP is 191.145.83.183/32 384 | SecurityGroupID: sg-12345 385 | IP: 191.145.83.183/32 386 | Port: 443 387 | Description: [aws-utils] steve home: HTTPS 388 | Found existing entry, and deleted it. 389 | Added entry with current details. 390 | ``` 391 | 392 | Usage: 393 | 394 | ```sh 395 | $ aws-utils whitelist-self /path/to/rules.json 396 | ``` 397 | 398 | 399 | ### `whoami` 400 | 401 | Show the current user, or assumed role. 402 | 403 | ``` 404 | $ aws-utils whoami 405 | aws-company-devops-prd 406 | ``` 407 | 408 | Or having assumed a role: 409 | 410 | ``` 411 | $ aws-utils whoami 412 | aws-company-role-prod-ro 413 | ``` 414 | 415 | 416 | 417 | ## Github Setup 418 | 419 | This repository is configured to run tests upon every commit, and when 420 | pull-requests are created/updated. The testing is carried out via 421 | [.github/run-tests.sh](.github/run-tests.sh) which is used by the 422 | [github-action-tester](https://github.com/skx/github-action-tester) action. 423 | 424 | Releases are automated in a similar fashion via [.github/build](.github/build), 425 | and the [github-action-publish-binaries](https://github.com/skx/github-action-publish-binaries) action. 426 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | --------------------------------------------------------------------------------