├── .gitignore ├── .goreleaser.yml ├── LICENSE ├── README.md ├── dist ├── CHANGELOG.md ├── checksums.txt ├── config.yaml ├── sshm_1.2.1_Darwin_i386.tar.gz ├── sshm_1.2.1_Darwin_x86_64.tar.gz ├── sshm_1.2.1_Linux_i386.tar.gz └── sshm_1.2.1_Linux_x86_64.tar.gz ├── go.mod ├── go.sum ├── img ├── screenshot1.png └── screenshot2.png └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | sshm 2 | .vscode -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # This is an example goreleaser.yaml file with some sane defaults. 2 | # Make sure to check the documentation at http://goreleaser.com 3 | builds: 4 | - env: 5 | - CGO_ENABLED=0 6 | archives: 7 | - replacements: 8 | darwin: Darwin 9 | linux: Linux 10 | windows: Windows 11 | 386: i386 12 | amd64: x86_64 13 | checksum: 14 | name_template: 'checksums.txt' 15 | snapshot: 16 | name_template: "{{ .Tag }}-next" 17 | changelog: 18 | sort: asc 19 | filters: 20 | exclude: 21 | - '^docs:' 22 | - '^test:' 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Thomas Labarussias 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SSHM 2 | 3 | ## Description 4 | 5 | Easy connect on EC2 instances thanks to AWS System Manager Agent. Just use your `~/.aws/profile` to easily select the instance you want to connect or on create a tunnel to. 6 | 7 | ## Why 8 | 9 | `SSH` is great, `SSH` is useful, we do love `SSH` as the sysdadmins we are but AWS doesn't let us add several keys except with a custom userdata or else. With `SSH` we have to set up a bastion and open security groups to some CIDR or even use a VPN. AWS SSM Agent permits to simplify this process, no need to share keys, no port to open, no instance to set up, you only use your IAM (with MFA) user and all which is done is logged in S3. See :https://docs.aws.amazon.com/systems-manager/latest/userguide/what-is-systems-manager.html. 10 | 11 | ## Prerequisites 12 | 13 | Install `session-manager-plugin` : https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html (minimum version : **1.1.35.0**) 14 | 15 | ## Usage 16 | 17 | ```bash 18 | Usage of sshm: 19 | -i string 20 | InstanceID for direct connection 21 | -lpn string 22 | Local Port Number for Proxy 23 | -p string 24 | Profile from ~/.aws/config 25 | -pn string 26 | Port Number for Proxy 27 | -r string 28 | Region, default is eu-west-1 (default "eu-west-1") 29 | ``` 30 | You can select your instance by ←, ↑, → ↓ and filter by *Tag:Name*, *InstanceId*, *Hostname*, *PrivateIp*, *PublicIp*, etc. **Enter** key to validate. 31 | 32 | If `-lpn` and `-pn` are specified, a tunnel is started : 33 | 34 | ```bash 35 | $ sshm -lpn 9999 -pn 80 36 | 37 | ... 38 | 39 | Starting session with SessionId: 1576756257508463700-0c3b74e1f450c5d5a 40 | Port 9999 opened for sessionId 1576756257508463700-0c3b74e1f450c5d5a. 41 | ``` 42 | 43 | Legend : 44 | * **Online** : all running instances with a SSM Agent connected 45 | * **Offline** : all instances with a SSM Agent disconnected (agent down or instance is stopped) 46 | * **Running** : all running instances with or without SSM Agent installed 47 | 48 | ## Output Example 49 | 50 | ![screenshot1](./img/screenshot1.png) 51 | ![screenshot2](./img/screenshot2.png) 52 | 53 | ## Build 54 | 55 | ```bash 56 | go build 57 | ``` 58 | This repository uses `go mod`, so don't `git clone` inside your `$GOPATH`. 59 | 60 | ## Author 61 | 62 | Thomas Labarussias (thomas.labarussias@fr.clara.net - https://github.com/Issif) -------------------------------------------------------------------------------- /dist/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Changelog 4 | 5 | 09475ad Merge pull request #4 from claranet/fix-forced-region 6 | 5d29af5 fix override of region 7 | 9a68700 fix wrong env var 8 | 9 | -------------------------------------------------------------------------------- /dist/checksums.txt: -------------------------------------------------------------------------------- 1 | fe120ec1502c02a1cf1b14989c40176dffaba226e32f38886e1b817ad11c2042 sshm_1.2.1_Linux_i386.tar.gz 2 | 6fe0a6e1ce32bbb750446a54ec4e4c1522e4bc1af764eac276d0a5327f3ef24d sshm_1.2.1_Linux_x86_64.tar.gz 3 | 509fb2a4e7df20c369d984581cf4cde753a5c54aaed94b6e9a5cf8775767acdf sshm_1.2.1_Darwin_x86_64.tar.gz 4 | bf8e072508d34fde28eda11b533e3c6bd1730402d3044cebd989ec36513f71a3 sshm_1.2.1_Darwin_i386.tar.gz 5 | -------------------------------------------------------------------------------- /dist/config.yaml: -------------------------------------------------------------------------------- 1 | project_name: sshm 2 | release: 3 | github: 4 | owner: claranet 5 | name: sshm 6 | name_template: '{{.Tag}}' 7 | scoop: 8 | name: sshm 9 | commit_author: 10 | name: goreleaserbot 11 | email: goreleaser@carlosbecker.com 12 | builds: 13 | - id: sshm 14 | goos: 15 | - linux 16 | - darwin 17 | goarch: 18 | - amd64 19 | - "386" 20 | goarm: 21 | - "6" 22 | targets: 23 | - linux_amd64 24 | - linux_386 25 | - darwin_amd64 26 | - darwin_386 27 | dir: . 28 | main: . 29 | ldflags: 30 | - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} 31 | -X main.builtBy=goreleaser 32 | binary: sshm 33 | env: 34 | - CGO_ENABLED=0 35 | lang: go 36 | archives: 37 | - id: default 38 | builds: 39 | - sshm 40 | name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm 41 | }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}' 42 | replacements: 43 | "386": i386 44 | amd64: x86_64 45 | darwin: Darwin 46 | linux: Linux 47 | windows: Windows 48 | format: tar.gz 49 | files: 50 | - licence* 51 | - LICENCE* 52 | - license* 53 | - LICENSE* 54 | - readme* 55 | - README* 56 | - changelog* 57 | - CHANGELOG* 58 | snapshot: 59 | name_template: '{{ .Tag }}-next' 60 | checksum: 61 | name_template: checksums.txt 62 | algorithm: sha256 63 | changelog: 64 | filters: 65 | exclude: 66 | - '^docs:' 67 | - '^test:' 68 | sort: asc 69 | dist: dist 70 | env_files: 71 | github_token: ~/.config/goreleaser/github_token 72 | gitlab_token: ~/.config/goreleaser/gitlab_token 73 | gitea_token: ~/.config/goreleaser/gitea_token 74 | github_urls: 75 | download: https://github.com 76 | gitlab_urls: 77 | download: https://gitlab.com 78 | -------------------------------------------------------------------------------- /dist/sshm_1.2.1_Darwin_i386.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/claranet/sshm/d8c45d0334df7eb68125fec0b98b5dd01018b85a/dist/sshm_1.2.1_Darwin_i386.tar.gz -------------------------------------------------------------------------------- /dist/sshm_1.2.1_Darwin_x86_64.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/claranet/sshm/d8c45d0334df7eb68125fec0b98b5dd01018b85a/dist/sshm_1.2.1_Darwin_x86_64.tar.gz -------------------------------------------------------------------------------- /dist/sshm_1.2.1_Linux_i386.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/claranet/sshm/d8c45d0334df7eb68125fec0b98b5dd01018b85a/dist/sshm_1.2.1_Linux_i386.tar.gz -------------------------------------------------------------------------------- /dist/sshm_1.2.1_Linux_x86_64.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/claranet/sshm/d8c45d0334df7eb68125fec0b98b5dd01018b85a/dist/sshm_1.2.1_Linux_x86_64.tar.gz -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com:claranet/sshm 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go v1.26.4 7 | github.com/manifoldco/promptui v0.6.0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/alecthomas/gometalinter v2.0.11+incompatible/go.mod h1:qfIpQGGz3d+NmgyPBqv+LSh50emm1pt72EtcX2vKYQk= 3 | github.com/alecthomas/gometalinter v3.0.0+incompatible h1:e9Zfvfytsw/e6Kd/PYd75wggK+/kX5Xn8IYDUKyc5fU= 4 | github.com/alecthomas/gometalinter v3.0.0+incompatible/go.mod h1:qfIpQGGz3d+NmgyPBqv+LSh50emm1pt72EtcX2vKYQk= 5 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E= 6 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= 7 | github.com/aws/aws-sdk-go v1.18.4 h1:zqlGJ5hF7CqFkQe5nprfaxo50QXsB1hf74QpQKPYtGY= 8 | github.com/aws/aws-sdk-go v1.18.4/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= 9 | github.com/aws/aws-sdk-go v1.26.4 h1:vQ1XmULJriCx8QTmvtEl511rskbZeTkr0xq59ky3kfI= 10 | github.com/aws/aws-sdk-go v1.26.4/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= 11 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 12 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= 13 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 14 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 15 | github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= 16 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 17 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 19 | github.com/golang/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= 20 | github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf h1:7+FW5aGwISbqUtkfmIpZJGRgNFg2ioYPvFaUxdqpDsg= 21 | github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf/go.mod h1:RpwtwJQFrIEPstU94h88MWPXP2ektJZ8cZ0YntAmXiE= 22 | github.com/gordonklaus/ineffassign v0.0.0-20180909121442-1003c8bd00dc h1:cJlkeAx1QYgO5N80aF5xRGstVsRQwgLR7uA2FnP1ZjY= 23 | github.com/gordonklaus/ineffassign v0.0.0-20180909121442-1003c8bd00dc/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= 24 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= 25 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= 26 | github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU= 27 | github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= 28 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 29 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 30 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 31 | github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw= 32 | github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= 33 | github.com/manifoldco/promptui v0.3.2 h1:rir7oByTERac6jhpHUPErHuopoRDvO3jxS+FdadEns8= 34 | github.com/manifoldco/promptui v0.3.2/go.mod h1:8JU+igZ+eeiiRku4T5BjtKh2ms8sziGpSYl1gN8Bazw= 35 | github.com/manifoldco/promptui v0.3.3-0.20190411181407-35bab80e16a4 h1:zNtxporN4V2l47+9kRel4xPgHv+0LsRgfU3aNgKwaUs= 36 | github.com/manifoldco/promptui v0.3.3-0.20190411181407-35bab80e16a4/go.mod h1:Qr+HrTC9bwokrkg5IsBOUZYbIFuLE8wzazmgj+/aLxw= 37 | github.com/manifoldco/promptui v0.6.0 h1:GuXmIdl5lhlamnWf3NbsKWYlaWyHABeStbD1LLsQMuA= 38 | github.com/manifoldco/promptui v0.6.0/go.mod h1:o9/C5VV8IPXxjxpl9au84MtQGIi5dwn7eldAgEdePPs= 39 | github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= 40 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 41 | github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= 42 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 43 | github.com/nicksnyder/go-i18n v1.10.1 h1:isfg77E/aCD7+0lD/D00ebR2MV5vgeQ276WYyDaCRQc= 44 | github.com/nicksnyder/go-i18n v1.10.1/go.mod h1:e4Di5xjP9oTVrC6y3C7C0HoSYXjSbhh/dU0eUV32nB4= 45 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 46 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 47 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 48 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 49 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 50 | github.com/tsenart/deadcode v0.0.0-20160724212837-210d2dc333e9 h1:vY5WqiEon0ZSTGM3ayVVi+twaHKHDFUVloaQ/wug9/c= 51 | github.com/tsenart/deadcode v0.0.0-20160724212837-210d2dc333e9/go.mod h1:q+QjxYvZ+fpjMXqs+XEriussHjSYqeXVnAdSV1tkMYk= 52 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3 h1:x/bBzNauLQAlE3fLku/xy92Y8QwKX5HZymrMz2IiKFc= 53 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 54 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b h1:MQE+LT/ABUuuvEZ+YQAMSXindAdUh7slEmAkup74op4= 55 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 56 | golang.org/x/tools v0.0.0-20181122213734-04b5d21e00f1 h1:bsEj/LXbv3BCtkp/rBj9Wi/0Nde4OMaraIZpndHAhdI= 57 | golang.org/x/tools v0.0.0-20181122213734-04b5d21e00f1/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 58 | gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20171010053543-63abe20a23e2 h1:5zOHKFi4LqGWG+3d+isqpbPrN/2yhDJnlO+BhRiuR6U= 59 | gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20171010053543-63abe20a23e2/go.mod h1:3HH7i1SgMqlzxCcBmUHW657sD4Kvv9sC3HpL3YukzwA= 60 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 61 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 62 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 63 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 64 | -------------------------------------------------------------------------------- /img/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/claranet/sshm/d8c45d0334df7eb68125fec0b98b5dd01018b85a/img/screenshot1.png -------------------------------------------------------------------------------- /img/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/claranet/sshm/d8c45d0334df7eb68125fec0b98b5dd01018b85a/img/screenshot2.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "flag" 7 | "log" 8 | "os" 9 | "regexp" 10 | "sort" 11 | "strconv" 12 | "strings" 13 | "syscall" 14 | 15 | "os/exec" 16 | "os/signal" 17 | 18 | "github.com/aws/aws-sdk-go/aws" 19 | "github.com/aws/aws-sdk-go/aws/credentials/stscreds" 20 | "github.com/aws/aws-sdk-go/aws/session" 21 | "github.com/aws/aws-sdk-go/service/ec2" 22 | "github.com/aws/aws-sdk-go/service/ssm" 23 | "github.com/manifoldco/promptui" 24 | ) 25 | 26 | type instance struct { 27 | InstanceID string 28 | ComputerName string 29 | PrivateIPAddress string 30 | PublicIPAddress string 31 | Name string 32 | InstanceState string 33 | AgentState string 34 | PlatformType string 35 | PlatformName string 36 | } 37 | 38 | var allInstances []instance 39 | var managedInstances []instance 40 | 41 | func main() { 42 | profile := flag.String("p", "", "Profile from ~/.aws/config") 43 | region := flag.String("r", "", "Region, default is eu-west-1") 44 | instance := flag.String("i", "", "InstanceID for direct connection") 45 | portNumber := flag.String("pn", "", "Port Number for Proxy") 46 | localPortNumber := flag.String("lpn", "", "Local Port Number for Proxy") 47 | flag.Parse() 48 | 49 | source := flag.Arg(0) 50 | destination := flag.Arg(1) 51 | 52 | if *profile == "" { 53 | if os.Getenv("AWS_PROFILE") != "" { 54 | *profile = os.Getenv("AWS_PROFILE") 55 | } else if os.Getenv("AWS_DEFAULT_PROFILE") != "" { 56 | *profile = os.Getenv("AWS_DEFAULT_PROFILE") 57 | } else { 58 | p := listProfiles() 59 | sort.Strings(p) 60 | *profile = selectProfile(p) 61 | } 62 | } 63 | 64 | // Create session (credentials from ~/.aws/config) 65 | sess := session.Must(session.NewSessionWithOptions(session.Options{ 66 | SharedConfigState: session.SharedConfigEnable, //enable use of ~/.aws/config 67 | AssumeRoleTokenProvider: stscreds.StdinTokenProvider, //ask for MFA if needed 68 | Profile: string(*profile), 69 | // Config: aws.Config{Region: aws.String(*region)}, 70 | })) 71 | 72 | R: 73 | switch { 74 | case *region != "": 75 | sess.Config.Region = aws.String(*region) 76 | case os.Getenv("AWS_REGION") != "": 77 | sess.Config.Region = aws.String(os.Getenv("AWS_REGION")) 78 | case os.Getenv("AWS_DEFAULT_REGION") != "": 79 | sess.Config.Region = aws.String(os.Getenv("AWS_DEFAULT_REGION")) 80 | case *sess.Config.Region != "": 81 | break R 82 | default: 83 | sess.Config.Region = aws.String("eu-west-1") 84 | } 85 | 86 | if *instance != "" { 87 | startSSH(*instance, region, profile, portNumber, localPortNumber, source, destination, sess) 88 | } else { 89 | allInstances = listAllInstances(sess) 90 | managedInstances = listManagedInstances(sess) 91 | if len(managedInstances) == 0 { 92 | log.Fatal("No available instance") 93 | } 94 | if selected := selectInstance(managedInstances); selected != "" { 95 | startSSH(selected, region, profile, portNumber, localPortNumber, source, destination, sess) 96 | } 97 | } 98 | } 99 | 100 | func listProfiles() []string { 101 | var profiles []string 102 | file, err := os.Open(os.Getenv("HOME") + "/.aws/config") 103 | if err != nil { 104 | log.Fatal(err) 105 | } 106 | defer file.Close() 107 | 108 | reg := regexp.MustCompile(`^\[profile `) 109 | 110 | scanner := bufio.NewScanner(file) 111 | for scanner.Scan() { 112 | if t := reg.MatchString(scanner.Text()); t == true { 113 | s := strings.TrimSuffix(reg.ReplaceAllString(scanner.Text(), "${1}"), "]") 114 | profiles = append(profiles, s) 115 | } 116 | } 117 | 118 | if err := scanner.Err(); err != nil { 119 | log.Fatal(err) 120 | } 121 | 122 | return profiles 123 | } 124 | 125 | func selectProfile(profiles []string) string { 126 | templates := &promptui.SelectTemplates{ 127 | // Label: ``, 128 | Active: `{{ "> " | cyan | bold }}{{ . | cyan | bold }}`, 129 | Inactive: ` {{ . }}`, 130 | } 131 | 132 | searcher := func(input string, index int) bool { 133 | j := profiles[index] 134 | name := strings.ToLower(j) 135 | input = strings.ToLower(input) 136 | 137 | return strings.Contains(name, input) 138 | } 139 | 140 | prompt := promptui.Select{ 141 | Label: "Profile", 142 | Items: profiles, 143 | Templates: templates, 144 | Size: 10, 145 | Searcher: searcher, 146 | StartInSearchMode: true, 147 | } 148 | 149 | selected, _, err := prompt.Run() 150 | if err != nil { 151 | os.Exit(0) 152 | } 153 | 154 | return profiles[selected] 155 | } 156 | 157 | func listAllInstances(sess *session.Session) []instance { 158 | client := ec2.New(sess) 159 | input := &ec2.DescribeInstancesInput{} 160 | response, err := client.DescribeInstances(input) 161 | if err != nil { 162 | log.Fatal(err.Error()) 163 | } 164 | 165 | var instances []instance 166 | for _, reservation := range response.Reservations { 167 | for _, i := range reservation.Instances { 168 | var e instance 169 | e.Name = "unnamed" 170 | for _, tag := range i.Tags { 171 | if *tag.Key == "Name" { 172 | e.Name = *tag.Value 173 | } 174 | } 175 | e.InstanceID = *i.InstanceId 176 | e.InstanceState = *i.State.Name 177 | e.PublicIPAddress = "None" 178 | if i.PublicIpAddress != nil { 179 | e.PublicIPAddress = *i.PublicIpAddress 180 | } else { 181 | e.PublicIPAddress = "N/A" 182 | } 183 | switch *i.State.Name { 184 | case "terminated", "shutting-down": 185 | // 186 | default: 187 | instances = append(instances, e) 188 | } 189 | } 190 | } 191 | return instances 192 | } 193 | 194 | func listManagedInstances(sess *session.Session) []instance { 195 | client := ssm.New(sess) 196 | input := &ssm.DescribeInstanceInformationInput{MaxResults: aws.Int64(50)} 197 | var instances []instance 198 | for { 199 | info, err := client.DescribeInstanceInformation(input) 200 | if err != nil { 201 | log.Println(err.Error()) 202 | } 203 | for _, i := range info.InstanceInformationList { 204 | var e instance 205 | e.InstanceID = *i.InstanceId 206 | e.AgentState = *i.PingStatus 207 | if *i.PingStatus == "Online" { 208 | e.ComputerName = *i.ComputerName 209 | e.PrivateIPAddress = *i.IPAddress 210 | e.PlatformType = *i.PlatformType 211 | e.PlatformName = *i.PlatformName + " " + *i.PlatformVersion 212 | } 213 | for _, j := range allInstances { 214 | if *i.InstanceId == j.InstanceID { 215 | e.Name = j.Name 216 | e.PublicIPAddress = j.PublicIPAddress 217 | e.InstanceState = j.InstanceState 218 | } 219 | } 220 | instances = append(instances, e) 221 | } 222 | if info.NextToken == nil { 223 | break 224 | } 225 | input.SetNextToken(*info.NextToken) 226 | } 227 | return instances 228 | } 229 | 230 | func selectInstance(managedInstances []instance) string { 231 | 232 | formattedInstancesList := getFormattedInstancesList(managedInstances) 233 | 234 | templates := &promptui.SelectTemplates{ 235 | // Label: ``, 236 | Active: `{{ if eq .AgentState "Online" }}{{ "> " | cyan | bold }}{{ .Name | cyan | bold }}{{ " | " | cyan | bold }}{{ .ComputerName | cyan | bold }}{{ " | " | cyan | bold }}{{ .InstanceID | cyan | bold }}{{ " | " | cyan | bold }}{{ .PrivateIPAddress | cyan | bold }}{{ else }}{{ "> " | red | bold }}{{ .Name | red | bold }}{{ " | " | red | bold }}{{ .ComputerName | red | bold }}{{ " | " | red | bold }}{{ .InstanceID | red | bold }}{{ " | " | red | bold }}{{ .PrivateIPAddress | red | bold }}{{ end }}`, 237 | Inactive: ` {{ if eq .AgentState "Online" }}{{ .Name }}{{ " | " }}{{ .ComputerName }}{{ " | " }}{{ .InstanceID }}{{ " | " }}{{ .PrivateIPAddress }}{{ else }}{{ .Name | red }}{{ " | " | red }}{{ .ComputerName | red }}{{ " | " | red}}{{ .InstanceID | red }}{{ " | " | red }}{{ .PrivateIPAddress | red }}{{ end }}`, 238 | Details: ` 239 | {{ "PublicIP: " }}{{ .PublicIPAddress }}{{ " | PlatformType: " }}{{ .PlatformType }}{{ " | PlatformName: " }}{{ .PlatformName }}{{ " | Agent: "}}{{ if eq .AgentState "Online" }}{{ .AgentState | bgGreen }}{{ else }}{{ .AgentState | bgRed }}{{ end }}{{ " | State: "}}{{ .InstanceState }}`, 240 | } 241 | 242 | searcher := func(input string, index int) bool { 243 | j := managedInstances[index] 244 | name := strings.Replace(strings.ToLower(j.InstanceID+j.ComputerName+j.PrivateIPAddress+j.PublicIPAddress+j.Name+j.InstanceState+j.AgentState+j.PlatformType+j.PlatformName), " ", "", -1) 245 | input = strings.Replace(strings.ToLower(input), " ", "", -1) 246 | 247 | return strings.Contains(name, input) 248 | } 249 | 250 | var countRunning, countOnline int 251 | for _, i := range allInstances { 252 | if i.InstanceState == "running" { 253 | countRunning++ 254 | } 255 | } 256 | for _, i := range managedInstances { 257 | if i.AgentState == "Online" { 258 | countOnline++ 259 | } 260 | } 261 | 262 | prompt := promptui.Select{ 263 | Label: "Online: " + strconv.Itoa(countOnline) + " | Offline: " + strconv.Itoa(len(managedInstances)-countOnline) + " | Running: " + strconv.Itoa(countRunning) + " ", 264 | Items: formattedInstancesList, 265 | Templates: templates, 266 | Size: 15, 267 | Searcher: searcher, 268 | StartInSearchMode: true, 269 | HideSelected: true, 270 | // HideHelp: true, 271 | } 272 | 273 | selected, _, err := prompt.Run() 274 | if err != nil { 275 | return "" 276 | } 277 | 278 | return managedInstances[selected].InstanceID 279 | } 280 | 281 | func startSSH(instanceID string, region, profile, portNumber, localPortNumber *string, source, destination string, sess *session.Session) { 282 | client := ssm.New(sess) 283 | input := &ssm.StartSessionInput{Target: aws.String(instanceID)} 284 | if *portNumber != "" && *localPortNumber != "" && source == "" { 285 | input.DocumentName = aws.String("AWS-StartPortForwardingSession") 286 | input.Parameters = map[string][]*string{"portNumber": []*string{aws.String(*portNumber)}, "localPortNumber": []*string{aws.String(*localPortNumber)}} 287 | } 288 | 289 | ssmSess, err := client.StartSession(input) 290 | if err != nil { 291 | log.Fatal(err.Error()) 292 | } 293 | payloadJSON, _ := json.Marshal(ssmSess) 294 | inputJSON, _ := json.Marshal(input) 295 | 296 | cmd := exec.Command("session-manager-plugin", string(payloadJSON), *region, "StartSession", *profile, string(inputJSON)) 297 | cmd.Stdout = os.Stdout 298 | cmd.Stdin = os.Stdin 299 | cmd.Stderr = os.Stderr 300 | signal.Ignore(syscall.SIGINT) 301 | cmd.Run() 302 | } 303 | 304 | func getFormattedInstancesList(managedInstances []instance) []instance { 305 | var size1, size2, size3, size4 int 306 | for _, i := range managedInstances { 307 | if len(i.Name) > size1 { 308 | size1 = len(i.Name) 309 | } 310 | if len(i.ComputerName) > size2 { 311 | size2 = len(i.ComputerName) 312 | } 313 | if len(i.InstanceID) > size3 { 314 | size3 = len(i.InstanceID) 315 | } 316 | if len(i.PrivateIPAddress) > size4 { 317 | size4 = len(i.PrivateIPAddress) 318 | } 319 | } 320 | 321 | var formattedInstancesList []instance 322 | for _, i := range managedInstances { 323 | var fi instance 324 | fi.Name = addSpaces(i.Name, size1) 325 | fi.ComputerName = addSpaces(i.ComputerName, size2) 326 | fi.InstanceID = addSpaces(i.InstanceID, size3) 327 | fi.PrivateIPAddress = addSpaces(i.PrivateIPAddress, size4) 328 | fi.PublicIPAddress = i.PublicIPAddress 329 | fi.InstanceState = i.InstanceState 330 | fi.AgentState = i.AgentState 331 | fi.PlatformType = i.PlatformType 332 | fi.PlatformName = i.PlatformName 333 | formattedInstancesList = append(formattedInstancesList, fi) 334 | } 335 | return formattedInstancesList 336 | } 337 | 338 | func addSpaces(text string, size int) string { 339 | for i := 0; size-len(text) > 0; i++ { 340 | text += " " 341 | } 342 | return text 343 | } 344 | --------------------------------------------------------------------------------