├── .bazelignore ├── .bazeliskrc ├── .bazelrc ├── .bazelversion ├── .chglog ├── CHANGELOG.tpl.md └── config.yml ├── .deepsource.toml ├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .github └── workflows │ ├── linters.yaml │ ├── release.yaml │ ├── test-linux.yaml │ └── test-macos.yaml ├── .gitignore ├── .golangci.yml ├── .pre-commit-config.yaml ├── BUILD ├── CHANGELOG.md ├── LICENSE ├── README.md ├── WORKSPACE ├── cmd ├── BUILD.bazel ├── list.go ├── root.go ├── session.go ├── ssh.go └── verify.go ├── docs ├── 00-get-started.md ├── 10-configuration.md ├── README.md └── usage │ ├── 00-list.md │ ├── 10-session.md │ ├── 20-ssh.md │ ├── 30-verify.md │ └── README.md ├── go.mod ├── go.sum ├── main.go ├── pkg ├── aws │ ├── BUILD.bazel │ ├── aws.go │ ├── aws_test.go │ ├── helpers │ │ ├── BUILD.bazel │ │ └── helpers.go │ ├── interface.go │ ├── list.go │ ├── list_test.go │ ├── log │ │ ├── BUILD.bazel │ │ └── log.go │ ├── session.go │ ├── session_test.go │ ├── ssh.go │ └── ssh_test.go ├── list │ ├── BUILD.bazel │ ├── list.go │ └── list_test.go ├── os │ ├── BUILD.bazel │ ├── ignore_signals.go │ └── ignore_signals_windows.go ├── session │ ├── BUILD.bazel │ ├── session.go │ └── session_test.go └── ssh │ ├── BUILD.bazel │ ├── ssh.go │ └── ssh_test.go ├── renovate.json ├── scripts └── pre-commit.sh └── tools ├── BUILD ├── fix_codecov.sh ├── get_workspace_status.sh └── repositories.bzl /.bazelignore: -------------------------------------------------------------------------------- 1 | .git 2 | scripts/ 3 | -------------------------------------------------------------------------------- /.bazeliskrc: -------------------------------------------------------------------------------- 1 | BAZELISK_NOJDK=1 2 | -------------------------------------------------------------------------------- /.bazelrc: -------------------------------------------------------------------------------- 1 | startup --expand_configs_in_place 2 | 3 | build --collect_code_coverage 4 | 5 | # Show us information about failures. 6 | build --verbose_failures 7 | test --test_output=errors 8 | 9 | # Include git version and other info 10 | build --stamp 11 | build --workspace_status_command tools/get_workspace_status.sh 12 | 13 | # Make /tmp hermetic 14 | build --sandbox_tmpfs_path=/tmp 15 | 16 | # Ensure that Bazel never runs as root, which can cause unit tests to fail. 17 | build --sandbox_fake_username 18 | 19 | test:unit --build_tests_only 20 | 21 | # Cross-compile pure Go 22 | build:cross:darwin_amd64 --platforms=@io_bazel_rules_go//go/toolchain:darwin_amd64 --cpu=amd64 23 | build:cross:windows_amd64 --platforms=@io_bazel_rules_go//go/toolchain:windows_amd64 --cpu=amd64 24 | build:cross:linux_amd64 --platforms=@io_bazel_rules_go//go/toolchain:linux_amd64 --cpu=amd64 25 | -------------------------------------------------------------------------------- /.bazelversion: -------------------------------------------------------------------------------- 1 | 4.2.1 2 | -------------------------------------------------------------------------------- /.chglog/CHANGELOG.tpl.md: -------------------------------------------------------------------------------- 1 | {{ if .Versions -}} 2 | 3 | ## [Unreleased] 4 | 5 | {{ if .Unreleased.CommitGroups -}} 6 | {{ range .Unreleased.CommitGroups -}} 7 | ### {{ .Title }} 8 | {{ range .Commits -}} 9 | - {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }} 10 | {{ end }} 11 | {{ end -}} 12 | {{ end -}} 13 | {{ end -}} 14 | 15 | {{ range .Versions }} 16 | 17 | ## {{ if .Tag.Previous }}[{{ .Tag.Name }}]{{ else }}{{ .Tag.Name }}{{ end }} - {{ datetime "2006-01-02" .Tag.Date }} 18 | {{ range .CommitGroups -}} 19 | ### {{ .Title }} 20 | {{ range .Commits -}} 21 | - {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }} 22 | {{ end }} 23 | {{ end -}} 24 | 25 | {{- if .RevertCommits -}} 26 | ### Reverts 27 | {{ range .RevertCommits -}} 28 | - {{ .Revert.Header }} 29 | {{ end }} 30 | {{ end -}} 31 | 32 | {{- if .NoteGroups -}} 33 | {{ range .NoteGroups -}} 34 | ### {{ .Title }} 35 | {{ range .Notes }} 36 | {{ .Body }} 37 | {{ end }} 38 | {{ end -}} 39 | {{ end -}} 40 | {{ end -}} 41 | 42 | {{- if .Versions }} 43 | [Unreleased]: {{ .Info.RepositoryURL }}/compare/{{ $latest := index .Versions 0 }}{{ $latest.Tag.Name }}...HEAD 44 | {{ range .Versions -}} 45 | {{ if .Tag.Previous -}} 46 | [{{ .Tag.Name }}]: {{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }} 47 | {{ end -}} 48 | {{ end -}} 49 | {{ end -}} 50 | -------------------------------------------------------------------------------- /.chglog/config.yml: -------------------------------------------------------------------------------- 1 | style: github 2 | template: CHANGELOG.tpl.md 3 | info: 4 | title: CHANGELOG 5 | repository_url: https://github.com/danmx/sigil 6 | options: 7 | commits: 8 | # filters: 9 | # Type: 10 | # - feat 11 | # - fix 12 | # - perf 13 | # - refactor 14 | commit_groups: 15 | # title_maps: 16 | # feat: Features 17 | # fix: Bug Fixes 18 | # perf: Performance Improvements 19 | # refactor: Code Refactoring 20 | header: 21 | pattern: "^(\\w*)(?:\\(([\\w\\$\\.\\-\\*\\s]*)\\))?\\:\\s(.*)$" 22 | pattern_maps: 23 | - Type 24 | - Scope 25 | - Subject 26 | notes: 27 | keywords: 28 | - BREAKING CHANGE 29 | -------------------------------------------------------------------------------- /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | test_patterns = ["**/*_test.go"] 4 | exclude_patterns = ["**/*_mock_test.go"] 5 | 6 | [[analyzers]] 7 | name = "go" 8 | enabled = true 9 | 10 | [analyzers.meta] 11 | import_path = "github.com/danmx/sigil" 12 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:21.04 2 | 3 | ARG BAZELISK_SHA256_HASH="4cb534c52cdd47a6223d4596d530e7c9c785438ab3b0a49ff347e991c210b2cd" 4 | ARG BAZELISK_VERSION="v1.10.1" 5 | 6 | RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -q -y \ 7 | python \ 8 | python3 \ 9 | wget \ 10 | ca-certificates \ 11 | gcc \ 12 | git \ 13 | && apt-get clean \ 14 | && wget -q -O /tmp/bazelisk https://github.com/bazelbuild/bazelisk/releases/download/${BAZELISK_VERSION}/bazelisk-linux-amd64 \ 15 | && echo "${BAZELISK_SHA256_HASH} /tmp/bazelisk" | sha256sum -c - \ 16 | && mv /tmp/bazelisk /usr/local/bin/bazel \ 17 | && chmod +x /usr/local/bin/bazel 18 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sigil", 3 | "build": { 4 | "dockerfile": "Dockerfile" 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /.github/workflows/linters.yaml: -------------------------------------------------------------------------------- 1 | name: Linters 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | linters: 7 | name: Linters 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | os: 13 | - ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: bazelbuild/setup-bazelisk@v1 17 | - name: Mount bazel cache # Optional 18 | uses: actions/cache@v2 19 | with: 20 | path: "~/.cache/bazel" 21 | key: bazel 22 | - name: gofmt 23 | run: bazel run :gazelle && bazel run @go_sdk//:bin/gofmt -- -e -l . 24 | shell: bash 25 | - name: golangci-lint 26 | run: bazel run @com_github_danmx_bazel_tools//golangci-lint:run -- run ./... 27 | shell: bash 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Bazel Release 2 | 3 | defaults: 4 | run: 5 | shell: bash 6 | 7 | on: 8 | push: 9 | tags: 10 | - '*' 11 | 12 | jobs: 13 | release: 14 | name: Release 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | os: 20 | - ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Mount bazel cache # Optional 24 | uses: actions/cache@v2 25 | with: 26 | path: "~/.cache/bazel" 27 | key: bazel 28 | - name: Login to Docker Hub 29 | uses: docker/login-action@v1 30 | with: 31 | username: ${{ secrets.DOCKERHUB_USERNAME }} 32 | password: ${{ secrets.DOCKERHUB_TOKEN }} 33 | - uses: bazelbuild/setup-bazelisk@v1 34 | - name: Run build release binaries 35 | run: | 36 | bazel build --config cross:linux_amd64 :sigil_linux-amd64 37 | bazel build --config cross:windows_amd64 :sigil_windows-amd64 38 | bazel build --config cross:darwin_amd64 :sigil_darwin-amd64 39 | - name: Run build release container image 40 | run: | 41 | bazel run --config cross:linux_amd64 :push-release-image 42 | bazel run --config cross:linux_amd64 :push-major-release-image 43 | bazel run --config cross:linux_amd64 :push-minor-release-image 44 | - name: Release 45 | uses: softprops/action-gh-release@v1 46 | with: 47 | files: | 48 | bazel-bin/sigil_linux-amd64.tar.gz 49 | bazel-bin/sigil_darwin-amd64.tar.gz 50 | bazel-bin/sigil_windows-amd64.zip 51 | -------------------------------------------------------------------------------- /.github/workflows/test-linux.yaml: -------------------------------------------------------------------------------- 1 | name: Bazel Linux Tests 2 | 3 | defaults: 4 | run: 5 | shell: bash 6 | 7 | on: [push, pull_request] 8 | 9 | jobs: 10 | tests: 11 | name: Tests 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: 17 | - ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Mount bazel cache # Optional 21 | uses: actions/cache@v2 22 | with: 23 | path: "~/.cache/bazel" 24 | key: bazel 25 | - name: Login to Docker Hub 26 | uses: docker/login-action@v1 27 | if: ${{ github.event_name != 'pull_request' }} 28 | with: 29 | username: ${{ secrets.DOCKERHUB_USERNAME }} 30 | password: ${{ secrets.DOCKERHUB_TOKEN }} 31 | - uses: bazelbuild/setup-bazelisk@v1 32 | - name: Run tests 33 | run: bazel test --config cross:linux_amd64 //... 34 | - name: Build 35 | run: bazel build --config cross:linux_amd64 :dev 36 | - name: Build container image 37 | run: bazel build --config cross:linux_amd64 :dev-image 38 | - name: Push dev container image 39 | run: bazel run --config cross:linux_amd64 :push-dev-image 40 | if: ${{ github.event_name != 'pull_request' }} 41 | - uses: codecov/codecov-action@v2 42 | with: 43 | files: ./coverage.txt 44 | directory: ./bazel-bin/ 45 | verbose: true 46 | -------------------------------------------------------------------------------- /.github/workflows/test-macos.yaml: -------------------------------------------------------------------------------- 1 | name: Bazel MacOS Tests 2 | 3 | defaults: 4 | run: 5 | shell: bash 6 | 7 | on: [pull_request] 8 | 9 | jobs: 10 | tests: 11 | name: Tests 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: 17 | - macos-latest 18 | - macos-10.15 19 | - macos-11.0 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Mount bazel cache # Optional 23 | uses: actions/cache@v2 24 | with: 25 | path: "~/.cache/bazel" 26 | key: bazel 27 | - uses: bazelbuild/setup-bazelisk@v1 28 | - name: Run tests 29 | run: bazel test --config cross:darwin_amd64 //... 30 | - name: Build 31 | run: bazel build --config cross:darwin_amd64 :dev 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | bin/ 3 | dist/ 4 | coverage.txt 5 | *.deb 6 | bazel-* 7 | *mock_test.go 8 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | deadline: 1m 3 | tests: true 4 | skip-dirs: 5 | - .git 6 | - bazel-* 7 | - tools 8 | - docs 9 | skip-files: 10 | - ".*_test\\.go$" 11 | skip-dirs-use-default: true 12 | modules-download-mode: readonly 13 | allow-parallel-runners: true 14 | linters-settings: 15 | dupl: 16 | threshold: 100 17 | goconst: 18 | min-len: 2 19 | min-occurrences: 2 20 | gocritic: 21 | enabled-tags: 22 | - diagnostic 23 | - experimental 24 | - opinionated 25 | - performance 26 | - style 27 | disabled-checks: 28 | - dupImport # https://github.com/go-critic/go-critic/issues/845 29 | - ifElseChain 30 | - octalLiteral 31 | - wrapperFunc 32 | gocyclo: 33 | min-complexity: 15 34 | goimports: 35 | local-prefixes: github.com/danmx/sigil 36 | golint: 37 | min-confidence: 0 38 | gomnd: 39 | settings: 40 | mnd: 41 | checks: argument,case,condition,return,operation,assign 42 | govet: 43 | check-shadowing: true 44 | maligned: 45 | suggest-new: true 46 | misspell: 47 | locale: UK 48 | 49 | linters: 50 | disable-all: true 51 | enable: 52 | - bodyclose 53 | - deadcode 54 | - depguard 55 | - dogsled 56 | - dupl 57 | - errcheck 58 | - goconst 59 | - gocritic 60 | - gocyclo 61 | - gofmt 62 | - goimports 63 | - gomnd 64 | - goprintffuncname 65 | - gosec 66 | - gosimple 67 | - govet 68 | - ineffassign 69 | - misspell 70 | - nakedret 71 | - staticcheck 72 | - structcheck 73 | - stylecheck 74 | - typecheck 75 | - unconvert 76 | - unparam 77 | - unused 78 | - varcheck 79 | - whitespace 80 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_stages: [commit] 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v3.1.0 5 | hooks: 6 | - id: check-yaml 7 | args: 8 | - --allow-multiple-documents 9 | - id: check-toml 10 | - id: end-of-file-fixer 11 | - id: trailing-whitespace 12 | - repo: local 13 | hooks: 14 | - id: fmt 15 | name: Run formatter 16 | entry: scripts/pre-commit.sh fmt 17 | language: system 18 | require_serial: true 19 | files: '.*\.go$' 20 | - id: lint 21 | name: Run linter 22 | entry: scripts/pre-commit.sh lint 23 | language: system 24 | require_serial: true 25 | files: '.*\.go$' 26 | stages: 27 | - push 28 | - id: update_deps 29 | name: Update Go dependencies in Bazel 30 | entry: scripts/pre-commit.sh update_deps 31 | language: system 32 | require_serial: true 33 | files: 'go\.(mod|sum)$' 34 | stages: 35 | - push 36 | - id: test 37 | name: Run tests 38 | entry: scripts/pre-commit.sh test 39 | language: system 40 | require_serial: true 41 | files: '.*\.go$' 42 | stages: 43 | - push 44 | - id: changelog 45 | name: Generate the changelog 46 | entry: scripts/pre-commit.sh changelog 47 | language: system 48 | require_serial: true 49 | stages: 50 | - push 51 | -------------------------------------------------------------------------------- /BUILD: -------------------------------------------------------------------------------- 1 | # gazelle:prefix github.com/danmx/sigil 2 | # gazelle:proto disable_global 3 | 4 | load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") 5 | load("@bazel_gazelle//:def.bzl", "gazelle") 6 | load("@bazel_tools//tools/build_defs/pkg:pkg.bzl", "pkg_tar") 7 | load("@io_bazel_rules_docker//container:image.bzl", "container_image") 8 | load("@io_bazel_rules_docker//container:layer.bzl", "container_layer") 9 | load("@io_bazel_rules_docker//docker:docker.bzl", "docker_push") 10 | 11 | package(default_visibility = ["//visibility:public"]) 12 | 13 | genrule( 14 | name = "concat-cov", 15 | srcs = glob(["bazel-out/**/testlogs/**/coverage.dat"]), 16 | outs = ["coverage.txt"], 17 | cmd_bash = "./$(location //tools:fix_codecov.sh) > \"$@\"", 18 | exec_tools = ["//tools:fix_codecov.sh"], 19 | ) 20 | 21 | gazelle( 22 | name = "gazelle", 23 | command = "fix", 24 | ) 25 | 26 | go_library( 27 | name = "go_default_library", 28 | srcs = ["main.go"], 29 | importpath = "github.com/danmx/sigil", 30 | visibility = ["//visibility:private"], 31 | x_defs = { 32 | "github.com/danmx/sigil/cmd.gitCommit": "{STABLE_GIT_COMMIT}", 33 | "github.com/danmx/sigil/cmd.appVersion": "{STABLE_VERSION}", 34 | }, 35 | deps = ["//cmd:go_default_library"], 36 | ) 37 | 38 | # Development 39 | go_binary( 40 | name = "dev", 41 | out = "dev/sigil", 42 | embed = [":go_default_library"], 43 | pure = "on", 44 | static = "on", 45 | x_defs = { 46 | "github.com/danmx/sigil/cmd.gitCommit": "{STABLE_GIT_COMMIT}", 47 | "github.com/danmx/sigil/cmd.appVersion": "{STABLE_VERSION}", 48 | "github.com/danmx/sigil/cmd.dev": "true", 49 | "github.com/danmx/sigil/cmd.logLevel": "debug", 50 | }, 51 | ) 52 | 53 | # Release 54 | go_binary( 55 | name = "release", 56 | out = "sigil", 57 | embed = [":go_default_library"], 58 | pure = "on", 59 | static = "on", 60 | ) 61 | 62 | pkg_tar( 63 | name = "sigil_darwin-amd64", 64 | srcs = [":release"], 65 | extension = "tar.gz", 66 | mode = "0o755", 67 | ) 68 | 69 | pkg_tar( 70 | name = "sigil_linux-amd64", 71 | srcs = [":release"], 72 | extension = "tar.gz", 73 | mode = "0o755", 74 | ) 75 | 76 | pkg_tar( 77 | name = "sigil_windows-amd64", 78 | srcs = [":release"], 79 | extension = "zip", 80 | mode = "0o755", 81 | ) 82 | 83 | # Include it in our base image as a tar. 84 | container_layer( 85 | name = "plugin-layer", 86 | debs = ["@session_manager_plugin_deb//file"], 87 | symlinks = {"/usr/bin/session-manager-plugin": "/usr/local/sessionmanagerplugin/bin/session-manager-plugin"}, 88 | visibility = ["//visibility:private"], 89 | ) 90 | 91 | container_layer( 92 | name = "dev-layer", 93 | directory = "/usr/bin", 94 | files = [":dev"], 95 | visibility = ["//visibility:private"], 96 | ) 97 | 98 | container_layer( 99 | name = "release-layer", 100 | files = [":release"], 101 | visibility = ["//visibility:private"], 102 | ) 103 | 104 | container_image( 105 | name = "dev-image", 106 | base = "@go_debug_image_base//image", 107 | cmd = ["--help"], 108 | entrypoint = ["sigil"], 109 | layers = [ 110 | "plugin-layer", 111 | "dev-layer", 112 | ], 113 | user = "nonroot", 114 | ) 115 | 116 | container_image( 117 | name = "release-image", 118 | base = "@go_debug_image_base//image", 119 | cmd = ["--help"], 120 | entrypoint = ["sigil"], 121 | layers = [ 122 | "plugin-layer", 123 | "release-layer", 124 | ], 125 | user = "nonroot", 126 | ) 127 | 128 | docker_push( 129 | name = "push-dev-image", 130 | image = ":dev-image", 131 | registry = "docker.io", 132 | repository = "danmx/sigil", 133 | tag = "dev", 134 | ) 135 | 136 | docker_push( 137 | name = "push-release-image", 138 | image = ":release-image", 139 | registry = "docker.io", 140 | repository = "danmx/sigil", 141 | tag = "{STABLE_VERSION}", 142 | ) 143 | 144 | docker_push( 145 | name = "push-major-release-image", 146 | image = ":release-image", 147 | registry = "docker.io", 148 | repository = "danmx/sigil", 149 | tag = "{STABLE_MAJOR_VERSION}", 150 | ) 151 | 152 | docker_push( 153 | name = "push-minor-release-image", 154 | image = ":release-image", 155 | registry = "docker.io", 156 | repository = "danmx/sigil", 157 | tag = "{STABLE_MINOR_VERSION}", 158 | ) 159 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## [Unreleased] 3 | 4 | ### Bazel 5 | - updating Go to 1.17.2 6 | - update gomock to version 1.3 7 | - updating rules_docker to 0.19.0 8 | - update gazelle to 0.23.0 9 | - update rules_go to version 0.28.0 10 | - update to version 4.2.1 11 | 12 | ### Chore 13 | - refactoring dev tools 14 | - adding VSCode dev container 15 | - **bazel:** removing reduntant rules_docker bits 16 | - **deps:** updating Go dependencies 17 | - **deps:** update dependency bazel_gazelle to v0.22.0 18 | - **deps:** update dependency io_bazel_rules_docker to v0.15.0 19 | - **deps:** updating AWS session-manager-plugin to version 1.2.7.0 20 | - **deps:** update l.gcr.io/google/bazel docker tag to v3.5.0 21 | - **deps:** update dependency io_bazel_rules_go to v0.24.5 22 | - **deps:** update module spf13/cobra to v1.1.1 23 | - **deps:** update module spf13/cobra to v1.1.0 24 | - **deps:** update module sirupsen/logrus to v1.7.0 25 | - **deps:** bumping go version to 1.15.3 26 | - **deps:** update dependency io_bazel_rules_go to v0.24.4 27 | - **deps:** updating Bazel rules 28 | - **deps:** update dependency bazel_gazelle to v0.22.2 29 | - **deps:** update module aws/aws-sdk-go to v1.34.32 30 | - **deps:** update dependency io_bazel_rules_go to v0.24.3 31 | - **deps:** update dependency bazel_gazelle to v0.22.1 32 | - **deps:** update dependency io_bazel_rules_go to v0.24.2 33 | - **deps:** update dependency io_bazel_rules_go to v0.24.1 34 | - **renovate:** scheduled checks 35 | - **tools:** updating dev tools 36 | 37 | ### Ci 38 | - using Github Actions 39 | 40 | ### Docker 41 | - update session manager plugin to 1.2.245.0 42 | 43 | ### Fic 44 | - **ci:** removing Drone integration 45 | 46 | ### Fix 47 | - profile to command line parser 48 | - **aws:** code style of log interface 49 | - **aws:** log interface 50 | - **bazel:** semi-hermitizing Go SDK 51 | - **ci:** disable steps that use secrets on PRs ([#187](https://github.com/danmx/sigil/issues/187)) 52 | - **drone:** updating drone.yml signature 53 | - **linters:** addressing golangci-lint issues 54 | 55 | ### Test 56 | - **aws:** adding unittests 57 | 58 | ### Update 59 | - **go:** version 1.15.2 60 | - **go:** version 1.15.1 61 | 62 | 63 | 64 | ## [0.7.0] - 2020-08-27 65 | ### Chore 66 | - **release:** version 0.7.0 67 | 68 | ### Feat 69 | - **cmd:** moving target to arg instead of separate flag 70 | 71 | ### Fix 72 | - **aws:** error log on a send ssh pub key failure 73 | - **cmd:** clarify ssh usage 74 | - **docs:** adding profile config entry description 75 | 76 | 77 | 78 | ## [0.6.1] - 2020-08-19 79 | ### Chore 80 | - **release:** version 0.6.1 81 | 82 | ### Fix 83 | - **aws:** reducing timeouts to speed up error feedback loop 84 | 85 | 86 | 87 | ## [0.6.0] - 2020-08-16 88 | ### Aws 89 | - customizing default retryer for AWS api calls 90 | 91 | ### Chore 92 | - removing deprecated name tag reference 93 | - **deps:** update golang.org/x/crypto commit hash to 75b2880 94 | - **deps:** update Go to 1.15 95 | - **deps:** bumping Go and tidying dependecies 96 | - **deps:** update module aws/aws-sdk-go to v1.34.2 97 | - **deps:** update dependency io_bazel_rules_go to v0.23.7 98 | - **deps:** update module spf13/viper to v1.7.1 99 | - **deps:** update module golang/mock to v1.4.4 100 | - **deps:** update golang.org/x/crypto commit hash to 123391f 101 | - **deps:** update l.gcr.io/google/bazel docker tag to v3.4.1 102 | - **deps:** update dependency io_bazel_rules_docker to v0.14.4 103 | - **deps:** x/crypto version bump 104 | - **release:** version 0.6.0 105 | 106 | ### Fix 107 | - **bazel:** docker rules 108 | - **pre-commit:** adding Gazelle run during fmt phase 109 | 110 | 111 | 112 | ## [0.5.3] - 2020-06-21 113 | ### Chore 114 | - **bazel:** bump to version 3.3.0 115 | - **deps:** update go modules in Bazel 116 | - **deps:** update module aws/aws-sdk-go to v1.32.6 117 | - **deps:** update l.gcr.io/google/bazel docker tag to v3.3.0 118 | - **pre-commit:** add go mod tidy to update_deps 119 | - **release:** version 0.5.3 120 | 121 | ### Rollback 122 | - name-tag target type change 123 | 124 | ### Update 125 | - **go:** version 1.14.4 126 | 127 | 128 | 129 | ## [0.5.2] - 2020-06-06 130 | ### Build 131 | - **deps:** bump github.com/aws/aws-sdk-go from 1.31.7 to 1.31.11 132 | - **deps:** bump github.com/aws/aws-sdk-go from 1.31.1 to 1.31.7 133 | - **deps:** bump github.com/stretchr/testify from 1.5.1 to 1.6.0 134 | 135 | ### Chore 136 | - **deps:** add renovate.json 137 | - **deps:** update module stretchr/testify to v1.6.1 138 | - **deps:** update dependency io_bazel_rules_go to v0.23.3 139 | - **deps:** update dependency io_bazel_rules_docker to v0.14.3 140 | - **deps:** update dependency bazel_gazelle to v0.21.1 141 | - **deps:** update golang.org/x/crypto commit hash to 70a84ac 142 | - **release:** 0.5.2 143 | 144 | ### Fix 145 | - **ssh:** error handling 146 | 147 | ### Update 148 | - **bazel:** to version 3.2.0 149 | - **deps:** Go dependecies in bazel 150 | 151 | 152 | 153 | ## [0.5.1] - 2020-05-23 154 | ### Add 155 | - **aws:** append AWS UA with sigil version 156 | 157 | ### Build 158 | - **deps:** bump github.com/aws/aws-sdk-go from 1.31.0 to 1.31.1 159 | - **deps:** bump github.com/aws/aws-sdk-go from 1.30.29 to 1.31.0 160 | - **deps:** bump github.com/aws/aws-sdk-go from 1.30.28 to 1.30.29 161 | - **deps:** bump github.com/aws/aws-sdk-go from 1.30.27 to 1.30.28 162 | - **deps:** bump github.com/aws/aws-sdk-go from 1.30.26 to 1.30.27 163 | - **deps:** bump github.com/aws/aws-sdk-go from 1.30.25 to 1.30.26 164 | - **deps:** bump gopkg.in/yaml.v2 from 2.2.8 to 2.3.0 165 | - **deps:** bump github.com/aws/aws-sdk-go from 1.30.24 to 1.30.25 166 | - **deps:** bump github.com/spf13/viper from 1.6.3 to 1.7.0 167 | - **deps:** bump github.com/aws/aws-sdk-go from 1.30.9 to 1.30.24 168 | - **deps:** bump github.com/sirupsen/logrus from 1.5.0 to 1.6.0 169 | 170 | ### Chore 171 | - using Bazel as a build system 172 | - **release:** 0.5.1 173 | 174 | ### Fix 175 | - app exit on failed session termination 176 | - version in makefile 177 | - **drone:** docker-release step 178 | - **lint:** addressing comments from deepsource.io 179 | 180 | ### Rm 181 | - **changelog:** merge PR commits 182 | - **drone:** release notes 183 | 184 | ### Update 185 | - dependencies 186 | 187 | 188 | 189 | ## [0.5.0] - 2020-04-18 190 | ### Chore 191 | - **pre-commit:** always run 192 | 193 | ### Delete 194 | - **doc:** manual pages 195 | 196 | ### Feat 197 | - adding support for environment variables 198 | - **ssh:** allowing custom temp. key directories 199 | 200 | ### Fix 201 | - **doc:** adding missing flag in ssh_config example 202 | - **lint:** removing broken bin path 203 | 204 | 205 | 206 | ## [0.4.1] - 2020-04-16 207 | ### Fix 208 | - **list:** filters 209 | 210 | ### Update 211 | - **version:** 0.4.1 212 | 213 | 214 | 215 | ## [0.4.0] - 2020-04-11 216 | ### Add 217 | - deepsource integration 218 | - **desc:** for tests 219 | - **pkg:** unit tests 220 | - **pre-commit:** adding pre-commit and dev section 221 | 222 | ### Feat 223 | - **golangci-lint:** added config file 224 | 225 | ### Fix 226 | - **ssh:** adding missing mfa token 227 | 228 | ### Update 229 | - **doc:** expanding documentation 230 | - **go:** bumping go and dependencies versions 231 | - **version:** to 0.4.0 232 | 233 | 234 | 235 | ## [0.3.3] - 2020-03-19 236 | 237 | 238 | ## [0.3.2] - 2020-03-11 239 | 240 | 241 | ## [0.3.1] - 2019-07-18 242 | 243 | 244 | ## [0.3.0] - 2019-07-13 245 | ### Stargate 246 | - Adding Support for SSH and SCP ([#44](https://github.com/danmx/sigil/issues/44)) 247 | 248 | 249 | 250 | ## [0.2.1] - 2019-05-14 251 | 252 | 253 | ## [0.2.0] - 2019-05-03 254 | 255 | 256 | ## [0.1.2] - 2019-04-29 257 | 258 | 259 | ## [0.1.1] - 2019-04-23 260 | 261 | 262 | ## [0.1.0] - 2019-04-23 263 | 264 | 265 | ## [0.0.8] - 2019-04-22 266 | 267 | 268 | ## [0.0.7] - 2019-04-16 269 | 270 | 271 | ## [0.0.6] - 2019-04-16 272 | 273 | 274 | ## [0.0.5] - 2019-04-15 275 | 276 | 277 | ## [0.0.4] - 2019-04-15 278 | 279 | 280 | ## [0.0.3] - 2019-03-19 281 | 282 | 283 | ## [0.0.2] - 2019-03-19 284 | 285 | 286 | ## 0.0.1 - 2019-03-18 287 | 288 | [Unreleased]: https://github.com/danmx/sigil/compare/0.7.0...HEAD 289 | [0.7.0]: https://github.com/danmx/sigil/compare/0.6.1...0.7.0 290 | [0.6.1]: https://github.com/danmx/sigil/compare/0.6.0...0.6.1 291 | [0.6.0]: https://github.com/danmx/sigil/compare/0.5.3...0.6.0 292 | [0.5.3]: https://github.com/danmx/sigil/compare/0.5.2...0.5.3 293 | [0.5.2]: https://github.com/danmx/sigil/compare/0.5.1...0.5.2 294 | [0.5.1]: https://github.com/danmx/sigil/compare/0.5.0...0.5.1 295 | [0.5.0]: https://github.com/danmx/sigil/compare/0.4.1...0.5.0 296 | [0.4.1]: https://github.com/danmx/sigil/compare/0.4.0...0.4.1 297 | [0.4.0]: https://github.com/danmx/sigil/compare/0.3.3...0.4.0 298 | [0.3.3]: https://github.com/danmx/sigil/compare/0.3.2...0.3.3 299 | [0.3.2]: https://github.com/danmx/sigil/compare/0.3.1...0.3.2 300 | [0.3.1]: https://github.com/danmx/sigil/compare/0.3.0...0.3.1 301 | [0.3.0]: https://github.com/danmx/sigil/compare/0.2.1...0.3.0 302 | [0.2.1]: https://github.com/danmx/sigil/compare/0.2.0...0.2.1 303 | [0.2.0]: https://github.com/danmx/sigil/compare/0.1.2...0.2.0 304 | [0.1.2]: https://github.com/danmx/sigil/compare/0.1.1...0.1.2 305 | [0.1.1]: https://github.com/danmx/sigil/compare/0.1.0...0.1.1 306 | [0.1.0]: https://github.com/danmx/sigil/compare/0.0.8...0.1.0 307 | [0.0.8]: https://github.com/danmx/sigil/compare/0.0.7...0.0.8 308 | [0.0.7]: https://github.com/danmx/sigil/compare/0.0.6...0.0.7 309 | [0.0.6]: https://github.com/danmx/sigil/compare/0.0.5...0.0.6 310 | [0.0.5]: https://github.com/danmx/sigil/compare/0.0.4...0.0.5 311 | [0.0.4]: https://github.com/danmx/sigil/compare/0.0.3...0.0.4 312 | [0.0.3]: https://github.com/danmx/sigil/compare/0.0.2...0.0.3 313 | [0.0.2]: https://github.com/danmx/sigil/compare/0.0.1...0.0.2 314 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sigil 2 | 3 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fdanmx%2Fsigil.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fdanmx%2Fsigil?ref=badge_shield) 4 | [![Build Status](https://cloud.drone.io/api/badges/danmx/sigil/status.svg)](https://cloud.drone.io/danmx/sigil) 5 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/b4725f567cbf46a493a5436ee698b571)](https://www.codacy.com/app/danmx/sigil?utm_source=github.com&utm_medium=referral&utm_content=danmx/sigil&utm_campaign=Badge_Grade) 6 | [![codecov](https://codecov.io/gh/danmx/sigil/branch/master/graph/badge.svg)](https://codecov.io/gh/danmx/sigil) 7 | [![DeepSource](https://static.deepsource.io/deepsource-badge-light-mini.svg)](https://deepsource.io/gh/danmx/sigil/?ref=repository-badge) 8 | 9 | ## Description 10 | 11 | > *Sigil* is the hub of the Great Wheel, a city at the center of the Outlands, the most balanced of neutral areas at the center of the planes. Also known as the "City of Doors" for the multitude of portals to other planes of existence and the Cage since those portals are the only way in or out, it is the setting for most of Planescape: Torment. 12 | 13 | *Sigil* is an AWS SSM Session manager client. Allowing access to EC2 instances without exposing any ports. 14 | 15 | ## Features 16 | 17 | - configuration files support (TOML, YAML, JSON, etc.) 18 | - support for different configuration profiles 19 | - lightweight [container image](https://hub.docker.com/r/danmx/sigil) 20 | - SSH and SCP support 21 | 22 | ## External dependencies 23 | 24 | ### Local 25 | 26 | - AWS [session-manager-plugin](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html) (version 1.1.17.0+ for SSH support) 27 | 28 | ### Remote 29 | 30 | - target EC2 instance must have AWS SSM Agent installed ([full guide](https://docs.aws.amazon.com/systems-manager/latest/userguide/ssm-agent.html)) (version 2.3.672.0+ for SSH support) 31 | - AWS [ec2-instance-connect](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-connect-set-up.html) to use SSH with your own and/or temporary keys 32 | - to support AWS SSM target EC2 instance profile should have **AmazonSSMManagedInstanceCore** managed IAM policy attached or a specific policy with similar permissions (check [About Policies for a Systems Manager Instance Profile](https://docs.aws.amazon.com/systems-manager/latest/userguide/setup-instance-profile.html) and [About Minimum S3 Bucket Permissions for SSM Agent](https://docs.aws.amazon.com/systems-manager/latest/userguide/ssm-agent-minimum-s3-permissions.html)) 33 | 34 | ## Documentation 35 | 36 | The manual can be found [here](docs/README.md). 37 | 38 | ## Installation 39 | 40 | ### Homebrew 41 | 42 | ```shell 43 | brew tap danmx/sigil 44 | brew install sigil 45 | ``` 46 | 47 | or 48 | 49 | ```shell 50 | brew install danmx/sigil/sigil 51 | ``` 52 | 53 | ### Docker 54 | 55 | ```shell 56 | docker pull danmx/sigil:0.7 57 | ``` 58 | 59 | ## Examples 60 | 61 | ### Usage 62 | 63 | Docker: 64 | 65 | ```shell 66 | docker run --rm -it -v "${HOME}"/.sigil:/home/nonroot/.sigil -v "${HOME}"/.aws:/home/.aws danmx/sigil:0.7 list --output-format wide 67 | ``` 68 | 69 | Binary: 70 | 71 | ```shell 72 | sigil -r eu-west-1 session --type instance-id i-xxxxxxxxxxxxxxxxx 73 | ``` 74 | 75 | Using with [aws-vault](https://github.com/99designs/aws-vault): 76 | 77 | ```shell 78 | aws-vault exec AWS_PROFILE -- sigil -r eu-west-1 session --type instance-id i-xxxxxxxxxxxxxxxxx 79 | ``` 80 | 81 | ### SSH integration 82 | 83 | Add an entry to your `ssh_config`: 84 | 85 | ```ssh_config 86 | Host i-* mi-* 87 | IdentityFile /tmp/sigil/%h/temp_key 88 | IdentitiesOnly yes 89 | ProxyCommand sigil ssh --port %p --pub-key /tmp/sigil/%h/temp_key.pub --gen-key-pair --os-user %r --gen-key-dir /tmp/sigil/%h/ %h 90 | Host *.compute.internal 91 | IdentityFile /tmp/sigil/%h/temp_key 92 | IdentitiesOnly yes 93 | ProxyCommand sigil ssh --type private-dns --port %p --pub-key /tmp/sigil/%h/temp_key.pub --gen-key-pair --os-user %r --gen-key-dir /tmp/sigil/%h/ %h 94 | ``` 95 | 96 | and run: 97 | 98 | ```shell 99 | ssh ec2-user@i-123456789 100 | ``` 101 | 102 | or 103 | 104 | ```shell 105 | ssh ec2-user@ip-10-0-0-5.eu-west-1.compute.internal 106 | ``` 107 | 108 | ### Config file 109 | 110 | By default configuration file is located in `${HOME}/.sigil/config.toml`. 111 | 112 | ```toml 113 | [default] 114 | type = "instance-id" 115 | output-format = "wide" 116 | region = "eu-west-1" 117 | profile = "dev" 118 | interactive = true 119 | ``` 120 | 121 | ## Changelog 122 | 123 | See [CHANGELOG.md](CHANGELOG.md) 124 | 125 | ## Build 126 | 127 | ### Binaries 128 | 129 | To build binaries (`development` and `release`) run: 130 | 131 | ```shell 132 | bazelisk build //... 133 | ``` 134 | 135 | To run specific build use: 136 | 137 | ```shell 138 | bazelisk build --config cross:[darwin|linux|windows]_amd64 :[dev|release] 139 | ``` 140 | 141 | for working Docker image: 142 | 143 | ```shell 144 | bazelisk build --config cross:linux_amd64 :[dev|release]-image 145 | ``` 146 | 147 | ### Container image 148 | 149 | To only build docker image run: 150 | 151 | ```shell 152 | bazelisk run :dev-image 153 | ``` 154 | 155 | It'll create a docker image tagged `bazel:dev-image`. 156 | 157 | ## Contributions 158 | 159 | All contributions are welcomed! 160 | 161 | ### Dev Dependencies 162 | 163 | - [pre-commit](https://pre-commit.com/) 164 | - [bazelisk](https://github.com/bazelbuild/bazelisk) 165 | 166 | ### Commits 167 | 168 | I'm trying to follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). 169 | 170 | ### Bootstraping 171 | 172 | ```sh 173 | pre-commit install 174 | pre-commit install --hook-type pre-push 175 | bazelisk sync 176 | ``` 177 | 178 | ## License 179 | 180 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fdanmx%2Fsigil.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fdanmx%2Fsigil?ref=badge_large) 181 | 182 | [Apache 2.0](LICENSE) 183 | 184 | ## Considerations 185 | 186 | *Sigil* was inspired by [xen0l's aws-gate](https://github.com/xen0l/aws-gate). 187 | -------------------------------------------------------------------------------- /WORKSPACE: -------------------------------------------------------------------------------- 1 | # gazelle:repository_macro tools/repositories.bzl%go_repositories 2 | 3 | workspace( 4 | name = "com_github_danmx_sigil", 5 | ) 6 | 7 | load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive", "http_file") 8 | 9 | # Golang 10 | http_archive( 11 | name = "io_bazel_rules_go", 12 | sha256 = "2b1641428dff9018f9e85c0384f03ec6c10660d935b750e3fa1492a281a53b0f", 13 | urls = [ 14 | "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.29.0/rules_go-v0.29.0.zip", 15 | "https://github.com/bazelbuild/rules_go/releases/download/v0.29.0/rules_go-v0.29.0.zip", 16 | ], 17 | ) 18 | 19 | load("@io_bazel_rules_go//go:deps.bzl", "go_download_sdk", "go_register_toolchains", "go_rules_dependencies") 20 | 21 | go_download_sdk( 22 | name = "go_sdk", 23 | version = "1.17.2", 24 | ) 25 | 26 | go_rules_dependencies() 27 | 28 | go_register_toolchains() 29 | 30 | # gazelle 31 | http_archive( 32 | name = "bazel_gazelle", 33 | sha256 = "de69a09dc70417580aabf20a28619bb3ef60d038470c7cf8442fafcf627c21cb", 34 | urls = [ 35 | "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.24.0/bazel-gazelle-v0.24.0.tar.gz", 36 | "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.24.0/bazel-gazelle-v0.24.0.tar.gz", 37 | ], 38 | ) 39 | 40 | load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies") 41 | 42 | gazelle_dependencies() 43 | 44 | # Container image 45 | http_archive( 46 | name = "io_bazel_rules_docker", 47 | sha256 = "92779d3445e7bdc79b961030b996cb0c91820ade7ffa7edca69273f404b085d5", 48 | strip_prefix = "rules_docker-0.20.0", 49 | urls = ["https://github.com/bazelbuild/rules_docker/releases/download/v0.20.0/rules_docker-v0.20.0.tar.gz"], 50 | ) 51 | 52 | load( 53 | "@io_bazel_rules_docker//repositories:repositories.bzl", 54 | container_repositories = "repositories", 55 | ) 56 | 57 | container_repositories() 58 | 59 | load("@io_bazel_rules_docker//repositories:deps.bzl", container_deps = "deps") 60 | 61 | container_deps() 62 | 63 | load( 64 | "@io_bazel_rules_docker//go:image.bzl", 65 | _go_image_repos = "repositories", 66 | ) 67 | 68 | _go_image_repos() 69 | 70 | load("//tools:repositories.bzl", "go_repositories") 71 | 72 | go_repositories() 73 | 74 | # GoMock 75 | http_archive( 76 | name = "com_github_jmhodges_bazel_gomock", 77 | sha256 = "82a5fb946d2eb0fed80d3d70c2556784ec6cb5c35cd65a1b5e93e46f99681650", 78 | strip_prefix = "bazel_gomock-1.3", 79 | urls = ["https://github.com/jmhodges/bazel_gomock/archive/v1.3.tar.gz"], 80 | ) 81 | 82 | # AWS Session Manager Plugin 83 | http_file( 84 | name = "session_manager_plugin_deb", 85 | downloaded_file_path = "session-manager-plugin.deb", 86 | sha256 = "f1c03d2aaad9f89f73fc70f1c1cdef0e2877a03b86cca3c8b5c97992c6344449", 87 | urls = ["https://s3.amazonaws.com/session-manager-downloads/plugin/1.2.245.0/ubuntu_64bit/session-manager-plugin.deb"], 88 | ) 89 | 90 | # golangci-lint & git-chglog 91 | http_archive( 92 | name = "com_github_danmx_bazel_tools", 93 | sha256 = "822a9c9f04c02418d17efcd58dd37c4890f8eb77e645f15281d43b7bbd2d1637", 94 | strip_prefix = "bazel-tools-0.3.1", 95 | urls = ["https://github.com/danmx/bazel-tools/archive/0.3.1.tar.gz"], 96 | ) 97 | 98 | load("@com_github_danmx_bazel_tools//git-chglog:deps.bzl", "git_chglog_dependencies") 99 | load("@com_github_danmx_bazel_tools//golangci-lint:deps.bzl", "golangci_lint_dependencies") 100 | 101 | git_chglog_dependencies() 102 | 103 | golangci_lint_dependencies() 104 | -------------------------------------------------------------------------------- /cmd/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = [ 6 | "list.go", 7 | "root.go", 8 | "session.go", 9 | "ssh.go", 10 | "verify.go", 11 | ], 12 | importpath = "github.com/danmx/sigil/cmd", 13 | visibility = ["//visibility:public"], 14 | deps = [ 15 | "//pkg/aws:go_default_library", 16 | "//pkg/list:go_default_library", 17 | "//pkg/session:go_default_library", 18 | "//pkg/ssh:go_default_library", 19 | "@com_github_mitchellh_go_homedir//:go_default_library", 20 | "@com_github_sirupsen_logrus//:go_default_library", 21 | "@com_github_spf13_cobra//:go_default_library", 22 | "@com_github_spf13_viper//:go_default_library", 23 | ], 24 | ) 25 | -------------------------------------------------------------------------------- /cmd/list.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/danmx/sigil/pkg/aws" 8 | "github.com/danmx/sigil/pkg/list" 9 | 10 | log "github.com/sirupsen/logrus" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var ( 15 | sessionFilters = map[string]string{ 16 | "after": "", 17 | "before": "", 18 | "target": "", 19 | "owner": "", 20 | } 21 | // listCmd represents the list command 22 | listCmd = &cobra.Command{ 23 | Use: "list [--type TYPE] ... { [--instance-ids IDs] [--instance-tags TAGS] | [--session-filters FILTERS] }", 24 | DisableFlagsInUseLine: true, 25 | Short: "List available EC2 instances or SSM sessions", 26 | Long: `Show list of all EC2 instances with AWS SSM Agent running or active SSM sessions. 27 | 28 | Supported groups of filters: 29 | - instances: 30 | - tags - list of tag keys with a list of values for given keys 31 | - ids - list of instastance ids 32 | - sessions: 33 | - after - the timestamp, in ISO-8601 Extended format, to see sessions that started after given date 34 | - before - the timestamp, in ISO-8601 Extended format, to see sessions that started before given date 35 | - target - an instance to which session connections have been made 36 | - owner - an AWS user account to see a list of sessions started by that user 37 | 38 | Filter format examples: 39 | [default.filters.session] 40 | after="2018-08-29T00:00:00Z" 41 | before="2019-08-29T00:00:00Z" 42 | target="i-xxxxxxxxxxxxxxxx1" 43 | owner="user@example.com" 44 | [default.filters.instance] 45 | ids=["i-xxxxxxxxxxxxxxxx1","i-xxxxxxxxxxxxxxxx2"] 46 | tags=[{key="Name",values=["WebApp1","WebApp2"]}] 47 | `, 48 | Aliases: []string{"ls", "l"}, 49 | Example: fmt.Sprintf(`%s list --output-format wide --instance-tags '[{"key":"Name","values":["Web","DB"]}]'`, appName), 50 | //nolint:dupl // deduplicating it wouldn't provide much value 51 | PreRunE: func(cmd *cobra.Command, args []string) error { 52 | // Config bindings 53 | for flag, lookup := range map[string]string{ 54 | "output-format": "output-format", 55 | "interactive": "interactive", 56 | "filters.session": "session-filters", 57 | "filters.instance.ids": "session-filters", 58 | "filters.instance.tags": "instance-tags", 59 | "list-type": "type", 60 | } { 61 | if err := cfg.BindPFlag(flag, cmd.Flags().Lookup(lookup)); err != nil { 62 | log.WithFields(log.Fields{ 63 | "flag": flag, 64 | "lookup": lookup, 65 | }).Error(err) 66 | return err 67 | } 68 | } 69 | // returns err 70 | return aws.VerifyDependencies() 71 | }, 72 | RunE: func(cmd *cobra.Command, args []string) error { 73 | var filters aws.Filters 74 | if err := cfg.UnmarshalKey("filters", &filters); err != nil { 75 | log.Error("failed unmarshaling filters") 76 | return fmt.Errorf("failed unmarshaling filters: %s", err) 77 | } 78 | outputFormat := cfg.GetString("output-format") 79 | profile := cfg.GetString("profile") 80 | region := cfg.GetString("region") 81 | interactive := cfg.GetBool("interactive") 82 | listType := cfg.GetString("list-type") 83 | instanceIDs := cfg.GetStringSlice("filters.instance.ids") 84 | mfaToken := cfg.GetString("mfa") 85 | trace := log.IsLevelEnabled(log.TraceLevel) 86 | // hack to get map[string]string from args 87 | // https://github.com/spf13/viper/issues/608 88 | if cmd.Flags().Changed("session-filters") { 89 | filters.Session = aws.SessionFilters{ 90 | After: sessionFilters["after"], 91 | Before: sessionFilters["before"], 92 | Target: sessionFilters["target"], 93 | Owner: sessionFilters["owner"], 94 | } 95 | } 96 | if cmd.Flags().Changed("instance-ids") { 97 | filters.Instance.IDs = instanceIDs 98 | } 99 | var tags []aws.TagValues 100 | if cmd.Flags().Changed("instance-tags") { 101 | if err := json.Unmarshal([]byte(cfg.GetString("filters.instance.tags")), &tags); err != nil { 102 | log.WithField("tags", cfg.GetString("filters.instance.tags")).Error("failed unmarshaling tags") 103 | return fmt.Errorf("failed unmarshaling tags: %s", err) 104 | } 105 | filters.Instance.Tags = tags 106 | } 107 | log.WithFields(log.Fields{ 108 | "filters": filters, 109 | "output-format": outputFormat, 110 | "region": region, 111 | "profile": profile, 112 | "mfa": mfaToken, 113 | "interactive": interactive, 114 | "type": listType, 115 | "instanceIDs": instanceIDs, 116 | "sessionFilters": sessionFilters, 117 | "tags": tags, 118 | "trace": trace, 119 | }).Debug("List inputs") 120 | input := &list.StartInput{ 121 | OutputFormat: &outputFormat, 122 | MFAToken: &mfaToken, 123 | Region: ®ion, 124 | Profile: &profile, 125 | Filters: &filters, 126 | Interactive: &interactive, 127 | Type: &listType, 128 | Trace: &trace, 129 | } 130 | err := list.Start(input) 131 | if err != nil { 132 | log.Error(err) 133 | return err 134 | } 135 | return nil 136 | }, 137 | DisableAutoGenTag: true, 138 | } 139 | ) 140 | 141 | func init() { 142 | rootCmd.AddCommand(listCmd) 143 | 144 | listCmd.Flags().String("output-format", list.FormatText, fmt.Sprintf("specify output format: %s/%s/%s/%s", list.FormatText, list.FormatWide, list.FormatJSON, list.FormatYAML)) 145 | listCmd.Flags().BoolP("interactive", "i", false, "pick an instance or a session from a list and start or terminate the session") 146 | listCmd.Flags().StringP("type", "t", list.TypeListInstances, fmt.Sprintf("specify list type: %s/%s", list.TypeListInstances, list.TypeListSessions)) 147 | listCmd.Flags().StringToStringVar(&sessionFilters, "session-filters", sessionFilters, "specify session filters to limit results") 148 | listCmd.Flags().StringSlice("instance-ids", []string{}, "specify instance ids to limit results") 149 | listCmd.Flags().String("instance-tags", "", "specify instance tags, in JSON format, to limit results") 150 | } 151 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | "strings" 8 | 9 | "github.com/danmx/sigil/pkg/aws" 10 | 11 | homedir "github.com/mitchellh/go-homedir" 12 | log "github.com/sirupsen/logrus" 13 | "github.com/spf13/cobra" 14 | "github.com/spf13/viper" 15 | ) 16 | 17 | var ( 18 | appName string = "sigil" 19 | // appVersion is the semantic appVersion (added at compile time) 20 | appVersion string 21 | // gitCommit is the git commit id (added at compile time) 22 | gitCommit string 23 | // logLevel level is setting loging level (added at compile time) 24 | logLevel string = "panic" 25 | // dev is turning a debug mode (added at compile time) 26 | dev string = "false" 27 | 28 | workDir string 29 | cfg *viper.Viper 30 | 31 | cfgFileName = "config" 32 | cfgType = "toml" 33 | workDirName = "." + appName 34 | 35 | // rootCmd represents the base command when called without any subcommands 36 | rootCmd = &cobra.Command{ 37 | Use: appName, 38 | Short: "AWS SSM Session manager client", 39 | Long: `A tool for establishing a session in EC2 instances with AWS SSM Agent installed`, 40 | Version: fmt.Sprintf("%s (build %s)", appVersion, gitCommit), 41 | DisableAutoGenTag: true, 42 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 43 | // returns err 44 | return aws.AppendUserAgent(appName + "/" + appVersion) 45 | }, 46 | } 47 | ) 48 | 49 | // Execute adds all child commands to the root command and sets flags appropriately. 50 | // This is called by main.main(). It only needs to happen once to the rootCmd. 51 | func Execute() error { 52 | // returns err 53 | return rootCmd.Execute() 54 | } 55 | 56 | func init() { 57 | // Set debug 58 | if dev == "true" { 59 | log.SetReportCaller(true) 60 | } 61 | // Set startup Log level 62 | if err := setLogLevel(logLevel); err != nil { 63 | log.WithFields(log.Fields{ 64 | "logLevel": logLevel, 65 | }).Fatal(err) 66 | } 67 | // Find home directory. 68 | home, err := homedir.Dir() 69 | if err != nil { 70 | fmt.Fprintln(os.Stderr, err) 71 | log.Fatal(err) 72 | } 73 | workDir = path.Join(home, workDirName) 74 | stat, err := os.Stat(workDir) 75 | if !(err == nil && stat.IsDir()) { 76 | if err := os.MkdirAll(workDir, 0750); err != nil { //nolint:gomnd // Linux file permissions 77 | fmt.Fprintln(os.Stderr, err) 78 | log.Fatal(err) 79 | } 80 | } 81 | 82 | // init config and env vars 83 | cobra.OnInitialize(func() { 84 | if err := initConfig(rootCmd); err != nil { 85 | fmt.Fprintln(os.Stderr, err) 86 | log.Fatal(err) 87 | } 88 | }) 89 | 90 | // Config file 91 | rootCmd.PersistentFlags().StringP("config", "c", "", "full config file path, supported formats: json/yaml/toml/hcl/props") 92 | rootCmd.PersistentFlags().StringP("config-profile", "p", "default", "pick the config profile") 93 | // Log level 94 | rootCmd.PersistentFlags().String("log-level", logLevel, "specify the log level: trace/debug/info/warn/error/fatal/panic") 95 | // AWS 96 | rootCmd.PersistentFlags().StringP("region", "r", "", "specify AWS region") 97 | rootCmd.PersistentFlags().String("profile", "", "specify AWS profile") 98 | rootCmd.PersistentFlags().StringP("mfa", "m", "", "specify MFA token") 99 | } 100 | 101 | // initConfig reads in config file and ENV variables if set 102 | func initConfig(cmd *cobra.Command) error { 103 | cfg = viper.New() 104 | // Environment variables 105 | cfg.SetEnvPrefix(appName) 106 | cfg.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) 107 | // Root config bindings 108 | for _, key := range []string{"config", "config-profile", "log-level"} { 109 | if err := cfg.BindEnv(key); err != nil { 110 | log.WithFields(log.Fields{ 111 | "env": key, 112 | }).Error(err) 113 | return err 114 | } 115 | if err := cfg.BindPFlag(key, cmd.PersistentFlags().Lookup(key)); err != nil { 116 | log.WithFields(log.Fields{ 117 | "flag": key, 118 | }).Error(err) 119 | return err 120 | } 121 | } 122 | cfgFile := cfg.GetString("config") 123 | cfgProfile := cfg.GetString("config-profile") 124 | logLevel := cfg.GetString("log-level") 125 | 126 | // Set Log level 127 | if err := setLogLevel(logLevel); err != nil { 128 | fmt.Fprintln(os.Stderr, err) 129 | log.WithFields(log.Fields{ 130 | "logLevel": logLevel, 131 | }).Error(err) 132 | return err 133 | } 134 | if cfgFile != "" { 135 | // Use config file from the flag. 136 | cfg.SetConfigFile(cfgFile) 137 | } else { 138 | // Search config in home directory with name from cfgFileName (without extension). 139 | cfg.AddConfigPath(workDir) 140 | cfg.SetConfigName(cfgFileName) 141 | cfg.SetConfigType(cfgType) 142 | } 143 | 144 | // If a config file is found, read it in. 145 | if err := cfg.ReadInConfig(); err == nil { 146 | log.WithFields(log.Fields{ 147 | "config": cfg.ConfigFileUsed(), 148 | }).Debug("Using config file") 149 | cfg, err = safeSub(cfg, cfgProfile) 150 | if err != nil { 151 | fmt.Fprintln(os.Stderr, err) 152 | log.WithFields(log.Fields{ 153 | "config-profile": cfgProfile, 154 | }).Error(err) 155 | return err 156 | } 157 | } 158 | 159 | // Rebinding config bindings that will be propagated to subcommands because of the subconfig (config profile) 160 | cfg.SetEnvPrefix(appName) 161 | if err := cfg.BindEnv("mfa"); err != nil { 162 | log.WithFields(log.Fields{ 163 | "env": "mfa", 164 | }).Error(err) 165 | return err 166 | } 167 | for _, key := range []string{"region", "config-profile", "profile"} { 168 | if err := cfg.BindPFlag(key, cmd.PersistentFlags().Lookup(key)); err != nil { 169 | log.WithFields(log.Fields{ 170 | "flag": key, 171 | }).Error(err) 172 | return err 173 | } 174 | } 175 | return nil 176 | } 177 | 178 | // because of https://github.com/spf13/viper/issues/616 179 | func safeSub(v *viper.Viper, profile string) (*viper.Viper, error) { 180 | subConfig := v.Sub(profile) 181 | if subConfig == nil { 182 | return nil, fmt.Errorf("config profile doesn't exist. Profile: %s", profile) 183 | } 184 | return subConfig, nil 185 | } 186 | 187 | // setLogLevel sets the log level 188 | func setLogLevel(level string) error { 189 | // Log level 190 | switch level { 191 | case "error": 192 | log.SetLevel(log.ErrorLevel) 193 | case "debug": 194 | log.SetLevel(log.DebugLevel) 195 | case "info": 196 | log.SetLevel(log.InfoLevel) 197 | case "warn": 198 | log.SetLevel(log.WarnLevel) 199 | case "fatal": 200 | log.SetLevel(log.FatalLevel) 201 | case "panic": 202 | log.SetLevel(log.PanicLevel) 203 | case "trace": 204 | log.SetLevel(log.TraceLevel) 205 | default: 206 | return fmt.Errorf("unsupported log level: %s", level) 207 | } 208 | return nil 209 | } 210 | -------------------------------------------------------------------------------- /cmd/session.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/danmx/sigil/pkg/aws" 7 | "github.com/danmx/sigil/pkg/session" 8 | 9 | log "github.com/sirupsen/logrus" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | // sessionCmd represents the session command 14 | var sessionCmd = &cobra.Command{ 15 | Use: "session [--type TYPE] ... TARGET", 16 | DisableFlagsInUseLine: true, 17 | Short: "Start a session", 18 | Long: `Start a new session in chosen EC2 instance.`, 19 | Aliases: []string{"sess", "s"}, 20 | Example: fmt.Sprintf("%s session --type instance-id i-xxxxxxxxxxxxxxxxx", appName), 21 | Args: cobra.MaximumNArgs(1), 22 | PreRunE: func(cmd *cobra.Command, args []string) error { 23 | if len(args) != 0 && cmd.Flags().Lookup("target").Changed { 24 | return fmt.Errorf("can't use both target argument (%s) and deprecated flag (%s)", args[0], cmd.Flags().Lookup("target").Value.String()) 25 | } 26 | // Config bindings 27 | for _, flag := range []string{"target", "type"} { 28 | if err := cfg.BindPFlag(flag, cmd.Flags().Lookup(flag)); err != nil { 29 | log.WithFields(log.Fields{ 30 | "flag": flag, 31 | }).Error(err) 32 | return err 33 | } 34 | } 35 | if len(args) > 0 { 36 | cfg.Set("target", args[0]) 37 | } 38 | // returns err 39 | return aws.VerifyDependencies() 40 | }, 41 | RunE: func(cmd *cobra.Command, args []string) error { 42 | target := cfg.GetString("target") 43 | targetType := cfg.GetString("type") 44 | profile := cfg.GetString("profile") 45 | region := cfg.GetString("region") 46 | mfaToken := cfg.GetString("mfa") 47 | trace := log.IsLevelEnabled(log.TraceLevel) 48 | log.WithFields(log.Fields{ 49 | "target": target, 50 | "type": targetType, 51 | "region": region, 52 | "profile": profile, 53 | "mfa": mfaToken, 54 | "trace": trace, 55 | }).Debug("Session inputs") 56 | input := &session.StartInput{ 57 | Target: &target, 58 | TargetType: &targetType, 59 | Region: ®ion, 60 | Profile: &profile, 61 | MFAToken: &mfaToken, 62 | Trace: &trace, 63 | } 64 | // returns err 65 | return session.Start(input) 66 | }, 67 | DisableAutoGenTag: true, 68 | } 69 | 70 | func init() { 71 | rootCmd.AddCommand(sessionCmd) 72 | 73 | sessionCmd.Flags().String("target", "", "specify the target depending on the type") 74 | // Deprecating the target flag 75 | err := sessionCmd.Flags().MarkDeprecated("target", "this flag will be deprecated in future releases, use args instead") 76 | if err != nil { 77 | log.WithField("flag", sessionCmd.Flags().Lookup("target")).Error(err) 78 | } 79 | sessionCmd.Flags().String("type", aws.TargetTypeInstanceID, fmt.Sprintf("specify target type: %s/%s/%s", aws.TargetTypeInstanceID, aws.TargetTypePrivateDNS, aws.TargetTypeName)) 80 | } 81 | -------------------------------------------------------------------------------- /cmd/ssh.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | 8 | "github.com/danmx/sigil/pkg/aws" 9 | "github.com/danmx/sigil/pkg/ssh" 10 | 11 | log "github.com/sirupsen/logrus" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | const tempKeyName = "temp_key" 16 | 17 | var ( 18 | portNum uint64 = 22 19 | sshCmd = &cobra.Command{ 20 | Use: "ssh [--type TYPE] ... [ { --gen-key-pair [--gen-key-dir DIR] | --pub-key PUB_KEY_PATH } ] TARGET", 21 | DisableFlagsInUseLine: true, 22 | Short: "Start ssh session", 23 | Long: `Start a new ssh for chosen EC2 instance.`, 24 | Args: cobra.MaximumNArgs(1), 25 | //nolint:dupl // deduplicating it wouldn't provide much value 26 | PreRunE: func(cmd *cobra.Command, args []string) error { 27 | if len(args) != 0 && cmd.Flags().Lookup("target").Changed { 28 | return fmt.Errorf("can't use both target argument (%s) and deprecated flag (%s)", args[0], cmd.Flags().Lookup("target").Value.String()) 29 | } 30 | // Config bindings 31 | for flag, lookup := range map[string]string{ 32 | "target": "target", 33 | "type": "type", 34 | "pub-key": "pub-key", 35 | "os-user": "os-user", 36 | "gen-key-pair": "gen-key-pair", 37 | "gen-key-dir": "gen-key-dir", 38 | } { 39 | if err := cfg.BindPFlag(flag, cmd.Flags().Lookup(lookup)); err != nil { 40 | log.WithFields(log.Fields{ 41 | "flag": flag, 42 | "lookup": lookup, 43 | }).Error(err) 44 | return err 45 | } 46 | } 47 | if len(args) > 0 { 48 | cfg.Set("target", args[0]) 49 | } 50 | // returns err 51 | return aws.VerifyDependencies() 52 | }, 53 | RunE: func(cmd *cobra.Command, args []string) error { 54 | target := cfg.GetString("target") 55 | targetType := cfg.GetString("type") 56 | profile := cfg.GetString("profile") 57 | region := cfg.GetString("region") 58 | pubKey := cfg.GetString("pub-key") 59 | OSUser := cfg.GetString("os-user") 60 | genKeyPair := cfg.GetBool("gen-key-pair") 61 | genKeyDir := cfg.GetString("gen-key-dir") 62 | mfaToken := cfg.GetString("mfa") 63 | trace := log.IsLevelEnabled(log.TraceLevel) 64 | if genKeyPair { 65 | stat, err := os.Stat(genKeyDir) 66 | if !(err == nil && stat.IsDir()) { 67 | if err = os.MkdirAll(genKeyDir, 0750); err != nil { //nolint:gomnd // Linux file permissions 68 | return err 69 | } 70 | } 71 | if err != nil { 72 | err = fmt.Errorf("failed creating directory for temporary keys: %e", err) 73 | log.WithFields(log.Fields{ 74 | "genKeyDir": genKeyDir, 75 | }).Error(err) 76 | return err 77 | } 78 | pubKey = path.Join(genKeyDir, tempKeyName+".pub") 79 | } 80 | log.WithFields(log.Fields{ 81 | "target": target, 82 | "type": targetType, 83 | "region": region, 84 | "profile": profile, 85 | "mfa": mfaToken, 86 | "pub-key": pubKey, 87 | "port": portNum, 88 | "os-user": OSUser, 89 | "gen-key-pair": genKeyPair, 90 | "gen-key-dir": genKeyDir, 91 | "trace": trace, 92 | }).Debug("ssh inputs") 93 | input := &ssh.StartInput{ 94 | Target: &target, 95 | TargetType: &targetType, 96 | PortNumber: &portNum, 97 | PublicKey: &pubKey, 98 | OSUser: &OSUser, 99 | GenKeyPair: &genKeyPair, 100 | Region: ®ion, 101 | Profile: &profile, 102 | MFAToken: &mfaToken, 103 | Trace: &trace, 104 | } 105 | // returns err 106 | return ssh.Start(input) 107 | }, 108 | DisableAutoGenTag: true, 109 | } 110 | ) 111 | 112 | func init() { 113 | rootCmd.AddCommand(sshCmd) 114 | 115 | sshCmd.Flags().String("target", "", "specify the target depending on the type") 116 | // Deprecating the target flag 117 | err := sshCmd.Flags().MarkDeprecated("target", "this flag will be deprecated in future releases, use args instead") 118 | if err != nil { 119 | log.WithField("flag", sshCmd.Flags().Lookup("target")).Error(err) 120 | } 121 | sshCmd.Flags().String("type", aws.TargetTypeInstanceID, fmt.Sprintf("specify target type: %s/%s/%s", aws.TargetTypeInstanceID, aws.TargetTypePrivateDNS, aws.TargetTypeName)) 122 | sshCmd.Flags().Bool("gen-key-pair", false, fmt.Sprintf("generate a temporary key pair that will be send and used. By default use %s as an identity file", path.Join(workDir, tempKeyName))) 123 | sshCmd.Flags().String("gen-key-dir", workDir, "the directory where temporary keys will be generated") 124 | sshCmd.Flags().String("os-user", "ec2-user", "specify an instance OS user which will be using sent public key") 125 | sshCmd.Flags().String("pub-key", "", "local public key that will be send to the instance, ignored when gen-key-pair is true") 126 | sshCmd.Flags().Uint64Var(&portNum, "port", portNum, "specify ssh port") 127 | } 128 | -------------------------------------------------------------------------------- /cmd/verify.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/danmx/sigil/pkg/aws" 7 | 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | // verifyCmd represents the verify command 12 | var verifyCmd = &cobra.Command{ 13 | Use: "verify", 14 | Short: "Verify if all external dependencies are available", 15 | Long: `This command will check if all dependecies are installed. 16 | Plugin documentation: https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html`, 17 | RunE: func(cmd *cobra.Command, args []string) error { 18 | if err := aws.VerifyDependencies(); err != nil { 19 | return err 20 | } 21 | fmt.Print("All dependencies are installed\n") 22 | return nil 23 | }, 24 | TraverseChildren: false, 25 | DisableFlagsInUseLine: true, 26 | DisableAutoGenTag: true, 27 | } 28 | 29 | func init() { 30 | rootCmd.AddCommand(verifyCmd) 31 | } 32 | -------------------------------------------------------------------------------- /docs/00-get-started.md: -------------------------------------------------------------------------------- 1 | # Get Started 2 | 3 | ## External dependencies 4 | 5 | To start using `sigil` you need to make sure you have all the necessary dependencies. 6 | 7 | ### Local 8 | 9 | - AWS [session-manager-plugin](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html) (version 1.1.17.0+ for SSH support) 10 | 11 | ### Remote 12 | 13 | - target EC2 instance must have AWS SSM Agent installed ([full guide](https://docs.aws.amazon.com/systems-manager/latest/userguide/ssm-agent.html)) (version 2.3.672.0+ for SSH support) 14 | - AWS [ec2-instance-connect](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-connect-set-up.html) to use SSH with your own and/or temporary keys 15 | - target EC2 instance profile should have **AmazonSSMManagedInstanceCore** managed IAM policy attached or a specific policy with similar permissions (check [About Policies for a Systems Manager Instance Profile](https://docs.aws.amazon.com/systems-manager/latest/userguide/setup-instance-profile.html) and [About Minimum S3 Bucket Permissions for SSM Agent](https://docs.aws.amazon.com/systems-manager/latest/userguide/ssm-agent-minimum-s3-permissions.html)) 16 | 17 | ## Download 18 | 19 | To download `sigil` you can use: 20 | 21 | ### Homebrew 22 | 23 | ```shell 24 | brew tap danmx/sigil 25 | brew install sigil 26 | ``` 27 | 28 | or 29 | 30 | ```shell 31 | brew install danmx/sigil/sigil 32 | ``` 33 | 34 | ### Docker 35 | 36 | ```shell 37 | docker pull danmx/sigil:0.7 38 | ``` 39 | 40 | ### Source code 41 | 42 | Pull the repository and build binaries. 43 | 44 | ```shell 45 | git clone https://github.com/danmx/sigil.git 46 | cd sigil 47 | bazelisk sync 48 | ``` 49 | 50 | For all binaries (`development` and `release`) and Docker image run: 51 | 52 | ```shell 53 | bazelisk build //... 54 | ``` 55 | 56 | To build a specific platform (Linux, Mac, Windows) use: 57 | 58 | ```shell 59 | bazelisk build --config cross:[darwin|linux|windows]_amd64 :[dev|release] 60 | ``` 61 | 62 | for working Docker image: 63 | 64 | ```shell 65 | bazelisk build --config cross:linux_amd64 :[dev|release]-image 66 | ``` 67 | 68 | To debug the image locally use `run` instead of `build`. 69 | -------------------------------------------------------------------------------- /docs/10-configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | The configuration varies depending on the command. For more defails (like default values) check [usage](usage/README.md) section or [man](man/sigil.md) pages. 4 | 5 | ## Global flags and corresponding environment variables 6 | 7 | ENV variables are case sensitive. 8 | 9 | | Flag | Environment variable | Description | 10 | | ----------------------- | :--------------------------------------------------------------------------------------------: | :----------------------------------------------------------------- | 11 | | `-c`/`--config` | `SIGIL_CONFIG` | full config file path, supported formats: json/yaml/toml/hcl/props | 12 | | `-p`/`--config-profile` | `SIGIL_CONFIG_PROFILE` | pick the config profile | 13 | | `--log-level` | `SIGIL_LOG_LEVEL` | specify the log level: trace/debug/info/warn/error/fatal/panic | 14 | | `-m`/`--mfa` | `SIGIL_MFA` | specify MFA token | 15 | | `--profile` | [`AWS_PROFILE`/`AWS_DEFAULT_PROFILE`](https://docs.aws.amazon.com/sdk-for-go/api/aws/session/) | specify AWS profile | 16 | | `-r`/`--region` | [`AWS_REGION`/`AWS_DEFAULT_REGION`](https://docs.aws.amazon.com/sdk-for-go/api/aws/session/) | specify AWS region | 17 | 18 | ## Config file 19 | 20 | Description of different values of the configuration file. 21 | 22 | | Parameter | Command(s) | Description | 23 | | ------------------------ | :-------------: | :--------------------------------------------------------------------------- | 24 | | `profile` | **all** | specify AWS profile | 25 | | `type` | `session`/`ssh` | specify target type | 26 | | `target` | `session`/`ssh` | specify the target depending on the type | 27 | | `os-user` | `ssh` | specify an instance OS user which will be using sent public key | 28 | | `port` | `ssh` | specify ssh port | 29 | | `gen-key-pair` | `ssh` | generate a temporary key pair that will be send and used | 30 | | `gen-key-dir` | `ssh` | the directory where temporary keys will be generated | 31 | | `pub-key` | `ssh` | local public key that will be send to the instance | 32 | | `output-format` | `list` | specify output format | 33 | | `interactive` | `list` | pick an instance or a session from a list and start or terminate the session | 34 | | `filters.session.after` | `list` | show only sessions that started after given datetime | 35 | | `filters.session.before` | `list` | show only sessions that started before given datetime | 36 | | `filters.session.target` | `list` | show only sessions for given target | 37 | | `filters.session.owner` | `list` | show only sessions owned by given owner | 38 | | `filters.instance.ids` | `list` | show only instances with matching IDs | 39 | | `filters.instance.tags` | `list` | show only instances with matching tags | 40 | 41 | ## Example 42 | 43 | An example of a fully configured `default` profile. 44 | 45 | ```toml 46 | [default] 47 | type = "name" 48 | target = "Worker" 49 | type = "instance-id" 50 | output-format = "text" 51 | region = "eu-west-1" 52 | profile = "dev" 53 | interactive = false 54 | os-user = "ec2-user" 55 | gen-key-pair = false 56 | gen-key-dir = "/tmp/sigil" 57 | pub-key = "~/.ssh/dev.pub" 58 | [default.filters.session] 59 | after="2018-08-29T00:00:00Z" 60 | before="2019-08-29T00:00:00Z" 61 | target="i-xxxxxxxxxxxxxxxx1" 62 | owner="user@example.com" 63 | [default.filters.instance] 64 | ids=["i-xxxxxxxxxxxxxxxx1","i-xxxxxxxxxxxxxxxx2"] 65 | tags = [ 66 | {key="Name", values=["Web","DB"] } 67 | ] 68 | ``` 69 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | - [get started](00-get-started.md) 4 | - [configuration](10-configuration.md) 5 | - [usage](usage/README.md) 6 | -------------------------------------------------------------------------------- /docs/usage/00-list.md: -------------------------------------------------------------------------------- 1 | # List 2 | 3 | This command list all active instances and sessions that match the filter. 4 | When interactive mode is enabled you can start a session in a given instance or terminate active session. 5 | 6 | ```console 7 | sigil list [--type TYPE] ... { [--instance-ids IDs] [--instance-tags TAGS] | [--session-filters FILTERS] } 8 | ``` 9 | 10 | [Man](../man/sigil_list.md) page 11 | 12 | ## Sample config 13 | 14 | Config file settings that affect the command 15 | 16 | ```toml 17 | [default] 18 | output-format = "text" 19 | region = "eu-west-1" 20 | profile = "dev" 21 | interactive = false 22 | [default.filters.session] 23 | after="2018-08-29T00:00:00Z" 24 | before="2019-08-29T00:00:00Z" 25 | target="i-xxxxxxxxxxxxxxxx1" 26 | owner="user@example.com" 27 | [default.filters.instance] 28 | ids=["i-xxxxxxxxxxxxxxxx1","i-xxxxxxxxxxxxxxxx2"] 29 | tags = [ 30 | {key="Name", values=["Web","DB"] } 31 | ] 32 | ``` 33 | 34 | ## Examples 35 | 36 | List instances 37 | 38 | ```console 39 | $ sigil list --instance-tags '[{"key":"Name","values":["Web","DB"]}]' 40 | Index Name Instance ID IP Address Private DNS Name 41 | 1 Web i-xxxxxxxxxxxxxxxx1 10.10.10.1 test1.local 42 | 2 DB i-xxxxxxxxxxxxxxxx2 10.10.10.2 test2.local 43 | ``` 44 | 45 | List sessions 46 | 47 | ```console 48 | $ sigil list -t sessions' 49 | Index Session ID Target Start Date 50 | 1 test-1234567890 i-xxxxxxxxxxxxxxxx1 2019-05-03T10:08:44Z 51 | ``` 52 | -------------------------------------------------------------------------------- /docs/usage/10-session.md: -------------------------------------------------------------------------------- 1 | # Session 2 | 3 | Start a new session in chosen EC2 instance based on its instance ID, name tag, or private DNS name. 4 | 5 | ```console 6 | sigil session [--type TYPE] ... TARGET 7 | ``` 8 | 9 | [Man](../man/sigil_session.md) page 10 | 11 | ## Sample config 12 | 13 | Config file settings that affect the command 14 | 15 | ```toml 16 | [default] 17 | type = "name" 18 | target = "Worker" 19 | region = "eu-west-1" 20 | profile = "dev" 21 | ``` 22 | 23 | ## Examples 24 | 25 | ```console 26 | $ sigil -r eu-west-1 session --type instance-id i-xxxxxxxxxxxxxxxxx 27 | Starting session with SessionId: example 28 | sh-4.2$ 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/usage/20-ssh.md: -------------------------------------------------------------------------------- 1 | # SSH 2 | 3 | Start a new ssh for chosen EC2 instance based on its instance ID, name tag, or private DNS name. 4 | 5 | ```console 6 | ssh [--type TYPE] ... [ { --gen-key-pair [--gen-key-dir DIR] | --pub-key PUB_KEY_PATH } ] TARGET 7 | ``` 8 | 9 | [Man](../man/sigil_ssh.md) page 10 | 11 | ## Sample config 12 | 13 | Config file settings that affect the command 14 | 15 | ```toml 16 | [default] 17 | type = "name" 18 | target = "Worker" 19 | region = "eu-west-1" 20 | profile = "dev" 21 | os-user = "ec2-user" 22 | gen-key-pair = false 23 | pub-key = "~/.ssh/dev.pub" 24 | ``` 25 | 26 | ## Examples 27 | 28 | `ssh_config` config file example: 29 | 30 | ```ssh_config 31 | Host i-* mi-* 32 | IdentityFile /tmp/sigil/%h/temp_key 33 | IdentitiesOnly yes 34 | ProxyCommand sigil ssh --port %p --pub-key /tmp/sigil/%h/temp_key.pub --gen-key-pair --os-user %r --gen-key-dir /tmp/sigil/%h/ %h 35 | Host *.compute.internal 36 | IdentityFile /tmp/sigil/%h/temp_key 37 | IdentitiesOnly yes 38 | ProxyCommand sigil ssh --type private-dns --port %p --pub-key /tmp/sigil/%h/temp_key.pub --gen-key-pair --os-user %r --gen-key-dir /tmp/sigil/%h/ %h 39 | ``` 40 | 41 | ```console 42 | $ ssh ec2-user@ip-10-0-0-5.eu-west-1.compute.internal 43 | Last login: Tue Jun 18 20:50:59 2019 from 10.0.0.5 44 | ... 45 | [ec2-user@example ~]$ 46 | ``` 47 | -------------------------------------------------------------------------------- /docs/usage/30-verify.md: -------------------------------------------------------------------------------- 1 | # Verify 2 | 3 | Check if all local dependencies are available. 4 | 5 | ```console 6 | sigil verify 7 | ``` 8 | 9 | [Man](../man/sigil_verify.md) page 10 | 11 | ## Examples 12 | 13 | Verify dependencies 14 | 15 | ```console 16 | $ sigil verify 17 | All dependencies are installed 18 | ``` 19 | -------------------------------------------------------------------------------- /docs/usage/README.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | List of available commands and their example usage: 4 | 5 | - [list](00-list.md) 6 | - [session](10-session.md) 7 | - [ssh](20-ssh.md) 8 | - [verify](30-verify.md) 9 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/danmx/sigil 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go v1.41.14 7 | github.com/golang/mock v1.6.0 8 | github.com/mitchellh/go-homedir v1.1.0 9 | github.com/sirupsen/logrus v1.8.1 10 | github.com/spf13/cobra v1.2.1 11 | github.com/spf13/viper v1.9.0 12 | github.com/stretchr/testify v1.7.0 13 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 14 | gopkg.in/yaml.v2 v2.4.0 15 | ) 16 | 17 | require ( 18 | github.com/davecgh/go-spew v1.1.1 // indirect 19 | github.com/fsnotify/fsnotify v1.5.1 // indirect 20 | github.com/hashicorp/hcl v1.0.0 // indirect 21 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 22 | github.com/jmespath/go-jmespath v0.4.0 // indirect 23 | github.com/magiconair/properties v1.8.5 // indirect 24 | github.com/mitchellh/mapstructure v1.4.2 // indirect 25 | github.com/pelletier/go-toml v1.9.4 // indirect 26 | github.com/pmezard/go-difflib v1.0.0 // indirect 27 | github.com/spf13/afero v1.6.0 // indirect 28 | github.com/spf13/cast v1.4.1 // indirect 29 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 30 | github.com/spf13/pflag v1.0.5 // indirect 31 | github.com/subosito/gotenv v1.2.0 // indirect 32 | golang.org/x/sys v0.0.0-20211031064116-611d5d643895 // indirect 33 | golang.org/x/text v0.3.7 // indirect 34 | gopkg.in/ini.v1 v1.63.2 // indirect 35 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 36 | ) 37 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/danmx/sigil/cmd" 7 | ) 8 | 9 | func main() { 10 | if err := cmd.Execute(); err != nil { 11 | os.Exit(1) //nolint:gomnd // custom error codes wouldn't provide much value 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /pkg/aws/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") 2 | load("@com_github_jmhodges_bazel_gomock//:gomock.bzl", "gomock") 3 | 4 | go_library( 5 | name = "go_default_library", 6 | srcs = [ 7 | "aws.go", 8 | "interface.go", 9 | "list.go", 10 | "session.go", 11 | "ssh.go", 12 | ], 13 | importpath = "github.com/danmx/sigil/pkg/aws", 14 | visibility = ["//visibility:public"], 15 | deps = [ 16 | "//pkg/aws/helpers:go_default_library", 17 | "//pkg/aws/log:go_default_library", 18 | "//pkg/os:go_default_library", 19 | "@com_github_aws_aws_sdk_go//aws:go_default_library", 20 | "@com_github_aws_aws_sdk_go//aws/client:go_default_library", 21 | "@com_github_aws_aws_sdk_go//aws/credentials/stscreds:go_default_library", 22 | "@com_github_aws_aws_sdk_go//aws/session:go_default_library", 23 | "@com_github_aws_aws_sdk_go//service/ec2:go_default_library", 24 | "@com_github_aws_aws_sdk_go//service/ec2/ec2iface:go_default_library", 25 | "@com_github_aws_aws_sdk_go//service/ec2instanceconnect:go_default_library", 26 | "@com_github_aws_aws_sdk_go//service/ec2instanceconnect/ec2instanceconnectiface:go_default_library", 27 | "@com_github_aws_aws_sdk_go//service/ssm:go_default_library", 28 | "@com_github_aws_aws_sdk_go//service/ssm/ssmiface:go_default_library", 29 | "@com_github_sirupsen_logrus//:go_default_library", 30 | ], 31 | ) 32 | 33 | go_test( 34 | name = "go_default_test", 35 | srcs = [ 36 | "aws_test.go", 37 | "ec2_aws_mock_test.go", 38 | "ec2instanceconnect_aws_mock_test.go", 39 | "helpers_mock_test.go", 40 | "list_test.go", 41 | "session_test.go", 42 | "ssh_test.go", 43 | "ssm_aws_mock_test.go", 44 | ], 45 | embed = [":go_default_library"], 46 | deps = [ 47 | "@com_github_aws_aws_sdk_go//aws:go_default_library", 48 | "@com_github_aws_aws_sdk_go//aws/request:go_default_library", 49 | "@com_github_aws_aws_sdk_go//service/ec2:go_default_library", 50 | "@com_github_aws_aws_sdk_go//service/ec2instanceconnect:go_default_library", 51 | "@com_github_aws_aws_sdk_go//service/ssm:go_default_library", 52 | "@com_github_golang_mock//gomock:go_default_library", 53 | "@com_github_stretchr_testify//assert:go_default_library", 54 | ], 55 | ) 56 | 57 | gomock( 58 | name = "mock_helpers", 59 | out = "helpers_mock_test.go", 60 | interfaces = [ 61 | "OSExecIface", 62 | "OSIface", 63 | ], 64 | library = "//pkg/aws/helpers:go_default_library", 65 | package = "aws", 66 | self_package = "github.com/danmx/sigil/pkg/aws", 67 | ) 68 | 69 | gomock( 70 | name = "mock_aws_ec2", 71 | out = "ec2_aws_mock_test.go", 72 | interfaces = [ 73 | "EC2API", 74 | ], 75 | library = "@com_github_aws_aws_sdk_go//service/ec2/ec2iface:go_default_library", 76 | package = "aws", 77 | self_package = "github.com/danmx/sigil/pkg/aws", 78 | ) 79 | 80 | gomock( 81 | name = "mock_aws_ssm", 82 | out = "ssm_aws_mock_test.go", 83 | interfaces = [ 84 | "SSMAPI", 85 | ], 86 | library = "@com_github_aws_aws_sdk_go//service/ssm/ssmiface:go_default_library", 87 | package = "aws", 88 | self_package = "github.com/danmx/sigil/pkg/aws", 89 | ) 90 | 91 | gomock( 92 | name = "mock_aws_ec2instanceconnect", 93 | out = "ec2instanceconnect_aws_mock_test.go", 94 | interfaces = [ 95 | "EC2InstanceConnectAPI", 96 | ], 97 | library = "@com_github_aws_aws_sdk_go//service/ec2instanceconnect/ec2instanceconnectiface:go_default_library", 98 | package = "aws", 99 | self_package = "github.com/danmx/sigil/pkg/aws", 100 | ) 101 | -------------------------------------------------------------------------------- /pkg/aws/aws.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "os/signal" 9 | "syscall" 10 | "time" 11 | 12 | "github.com/danmx/sigil/pkg/aws/helpers" 13 | logger "github.com/danmx/sigil/pkg/aws/log" 14 | sigilOS "github.com/danmx/sigil/pkg/os" 15 | 16 | "github.com/aws/aws-sdk-go/aws" 17 | "github.com/aws/aws-sdk-go/aws/client" 18 | "github.com/aws/aws-sdk-go/aws/credentials/stscreds" 19 | "github.com/aws/aws-sdk-go/aws/session" 20 | "github.com/aws/aws-sdk-go/service/ec2" 21 | "github.com/aws/aws-sdk-go/service/ec2/ec2iface" 22 | log "github.com/sirupsen/logrus" 23 | ) 24 | 25 | // Provider contains necessary components like the session 26 | type Provider struct { 27 | filters Filters 28 | session *session.Session 29 | awsProfile string 30 | } 31 | 32 | // Config contains provider's configuration 33 | type Config struct { 34 | Filters Filters 35 | Region string 36 | Profile string 37 | MFAToken string 38 | Trace bool 39 | } 40 | 41 | // Filters grouped per type 42 | type Filters struct { 43 | Instance InstanceFilters `mapstructure:"instance"` 44 | Session SessionFilters `mapstructure:"session"` 45 | } 46 | 47 | // Instance contain information about the EC2 instance 48 | type Instance struct { 49 | Hostname string `json:"hostname" yaml:"hostname"` 50 | IPAddress string `json:"ip_address" yaml:"ip_address"` 51 | ID string `json:"id" yaml:"id"` 52 | PrivateDNSName string `json:"private_dns_name" yaml:"private_dns_name"` 53 | Name string `json:"name" yaml:"name"` 54 | OSName string `json:"os_name" yaml:"os_name"` 55 | OSType string `json:"os_type" yaml:"os_type"` 56 | OSVersion string `json:"os_version" yaml:"os_version"` 57 | } 58 | 59 | // InstanceFilters contain all types of filters used to limit results 60 | type InstanceFilters struct { 61 | IDs []string `json:"ids" mapstructure:"ids,remain"` 62 | Tags []TagValues `json:"tags" mapstructure:"tags,remain"` 63 | } 64 | 65 | // TagValues contain list of values for specific key 66 | type TagValues struct { 67 | Key string `json:"key" mapstructure:"key,remain"` 68 | Values []string `json:"values" mapstructure:"values,remain"` 69 | } 70 | 71 | // Session contains information about SSM sessions 72 | type Session struct { 73 | SessionID string `json:"session_id" yaml:"session_id"` 74 | Target string `json:"target" yaml:"target"` 75 | Status string `json:"status" yaml:"status"` 76 | StartDate string `json:"start_date" yaml:"start_date"` 77 | Owner string `json:"owner" yaml:"owner"` 78 | } 79 | 80 | // SessionFilters for SSM sessions 81 | type SessionFilters struct { 82 | After string `json:"after" mapstructure:"after,remain"` 83 | Before string `json:"before" mapstructure:"before,remain"` 84 | Target string `json:"target" mapstructure:"target,remain"` 85 | Owner string `json:"owner" mapstructure:"owner,remain"` 86 | } 87 | 88 | const ( 89 | execEnvVar = "AWS_EXECUTION_ENV" 90 | maxResults int64 = 50 91 | pluginName = "session-manager-plugin" 92 | // API calls retry configuration 93 | numMaxRetries = 5 94 | minRetryDelay = 10 * time.Millisecond 95 | minThrottleDelay = 500 * time.Millisecond 96 | maxRetryDelay = 1 * time.Second 97 | maxThrottleDelay = 4 * time.Second 98 | // TargetTypeInstanceID points to an instance ID type 99 | TargetTypeInstanceID = "instance-id" 100 | // TargetTypePrivateDNS points to a private DNS type 101 | TargetTypePrivateDNS = "private-dns" 102 | // TargetTypeName points to a name type 103 | TargetTypeName = "name" 104 | ) 105 | 106 | // NewWithConfig will generate an AWS Provider with given configuration 107 | func (p *Provider) NewWithConfig(c *Config) error { 108 | options := session.Options{ 109 | SharedConfigState: session.SharedConfigEnable, 110 | AssumeRoleTokenProvider: mfaTokenProvider(c.MFAToken), 111 | Profile: c.Profile, 112 | } 113 | awsConfig := aws.NewConfig() 114 | if c.Trace { 115 | awsConfig.LogLevel = aws.LogLevel(aws.LogDebugWithRequestRetries) 116 | awsConfig.Logger = logger.NewTraceLogger() 117 | } 118 | awsConfig.Retryer = client.DefaultRetryer{ 119 | NumMaxRetries: numMaxRetries, 120 | MinRetryDelay: minRetryDelay, 121 | MinThrottleDelay: minThrottleDelay, 122 | MaxRetryDelay: maxRetryDelay, 123 | MaxThrottleDelay: maxThrottleDelay, 124 | } 125 | awsConfig.Region = aws.String(c.Region) 126 | options.Config = *awsConfig 127 | sess, err := session.NewSessionWithOptions(options) 128 | if err != nil { 129 | log.WithFields(log.Fields{ 130 | "err": err, 131 | "options": options, 132 | }).Error("Failed starting a session") 133 | return err 134 | } 135 | p.session = sess 136 | p.awsProfile = c.Profile 137 | p.filters = c.Filters 138 | return nil 139 | } 140 | 141 | // VerifyDependencies will check all necessary dependencies 142 | func VerifyDependencies() error { 143 | return verifyDependencies(new(helpers.Helpers)) 144 | } 145 | 146 | func verifyDependencies(exechelpers helpers.OSExecIface) error { 147 | o, err := exechelpers.LookPath(pluginName) 148 | if err != nil { 149 | err = fmt.Errorf("required plugin not found: %s", err) 150 | log.Error(err) 151 | return err 152 | } 153 | log.WithFields(log.Fields{ 154 | "plugin": pluginName, 155 | "path": o, 156 | }).Debugf("%s is installed successfully in %s\n", pluginName, o) 157 | return nil 158 | } 159 | 160 | // AppendUserAgent will add given suffix to HTTP client's user agent 161 | func AppendUserAgent(suffix string) error { 162 | return appendUserAgent(new(helpers.Helpers), suffix) 163 | } 164 | 165 | func appendUserAgent(oshelpers helpers.OSIface, suffix string) error { 166 | value, set := oshelpers.LookupEnv(execEnvVar) 167 | if set { 168 | value += "/" 169 | } 170 | value += suffix 171 | return oshelpers.Setenv(execEnvVar, value) 172 | } 173 | 174 | func (p *Provider) getInstance(targetType, target string) (*ec2.Instance, error) { 175 | return getInstance(ec2.New(p.session), targetType, target) 176 | } 177 | 178 | func getInstance(ec2Client ec2iface.EC2API, targetType, target string) (*ec2.Instance, error) { 179 | if target == "" { 180 | err := errors.New("no target") 181 | log.WithFields(log.Fields{ 182 | "target": target, 183 | }).Error(err) 184 | return nil, err 185 | } 186 | filters, err := getFilters(targetType, target) 187 | if err != nil { 188 | return nil, err 189 | } 190 | instance, err := getFirstInstance(ec2Client, filters) 191 | if err != nil { 192 | log.WithFields(log.Fields{ 193 | "filters": filters, 194 | }).Error("failed getting the first instance") 195 | return nil, err 196 | } 197 | if instance == nil { 198 | err := fmt.Errorf("no instance that matches the target (%s) and the type (%s)", target, targetType) 199 | log.WithFields(log.Fields{ 200 | "targetType": targetType, 201 | "taget": target, 202 | }).Info(err) 203 | return nil, err 204 | } 205 | return instance, nil 206 | } 207 | 208 | func getFilters(targetType, target string) ([]*ec2.Filter, error) { 209 | var filters []*ec2.Filter 210 | switch targetType { 211 | case TargetTypeInstanceID: 212 | filters = []*ec2.Filter{ 213 | { 214 | Name: aws.String("instance-id"), 215 | Values: []*string{&target}, 216 | }, 217 | { 218 | Name: aws.String("instance-state-name"), 219 | Values: []*string{aws.String("running")}, 220 | }, 221 | } 222 | case TargetTypePrivateDNS: 223 | filters = []*ec2.Filter{ 224 | { 225 | Name: aws.String("private-dns-name"), 226 | Values: []*string{&target}, 227 | }, 228 | { 229 | Name: aws.String("instance-state-name"), 230 | Values: []*string{aws.String("running")}, 231 | }, 232 | } 233 | case TargetTypeName: 234 | filters = []*ec2.Filter{ 235 | { 236 | Name: aws.String("tag:Name"), 237 | Values: []*string{&target}, 238 | }, 239 | { 240 | Name: aws.String("instance-state-name"), 241 | Values: []*string{aws.String("running")}, 242 | }, 243 | } 244 | default: 245 | log.WithFields(log.Fields{ 246 | "target": target, 247 | "targetType": targetType, 248 | }).Error("unsupported target type") 249 | return nil, fmt.Errorf("unsupported target type: %s", targetType) 250 | } 251 | return filters, nil 252 | } 253 | 254 | func getFirstInstance(ec2Client ec2iface.EC2API, filters []*ec2.Filter) (*ec2.Instance, error) { 255 | input := &ec2.DescribeInstancesInput{ 256 | Filters: filters, 257 | MaxResults: aws.Int64(maxResults), 258 | } 259 | var target *ec2.Instance 260 | err := ec2Client.DescribeInstancesPages(input, 261 | func(page *ec2.DescribeInstancesOutput, lastPage bool) bool { 262 | for _, reservation := range page.Reservations { 263 | for _, instance := range reservation.Instances { 264 | target = instance 265 | // Escape the function 266 | return false 267 | } 268 | } 269 | return !lastPage 270 | }) 271 | if err != nil { 272 | log.WithFields(log.Fields{ 273 | "filters": filters, 274 | "DescribeInstancesInput": input, 275 | }).Error("failed DescribeInstancesPages") 276 | return nil, err 277 | } 278 | return target, nil 279 | } 280 | 281 | func mfaTokenProvider(token string) func() (string, error) { 282 | log.WithFields(log.Fields{ 283 | "token": token, 284 | }).Debug("Get MFA Token Provider") 285 | if token == "" { 286 | return stscreds.StdinTokenProvider 287 | } 288 | return func() (string, error) { 289 | return token, nil 290 | } 291 | } 292 | 293 | func runSessionPluginManager(payloadJSON, region, profile, inputJSON, endpoint string) error { 294 | log.WithFields(log.Fields{ 295 | "payload": payloadJSON, 296 | "region": region, 297 | "profile": profile, 298 | "input": inputJSON, 299 | "endpoint": endpoint, 300 | }).Debug("Inspect session-manager-plugin args") 301 | // TODO allowing logging 302 | // https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html#configure-logs-linux 303 | // https://github.com/aws/aws-cli/blob/5f16b26/awscli/customizations/sessionmanager.py#L83-L89 304 | shell := exec.Command(pluginName, payloadJSON, region, "StartSession", profile, inputJSON, endpoint) 305 | shell.Stdout = os.Stdout 306 | shell.Stdin = os.Stdin 307 | shell.Stderr = os.Stderr 308 | sigilOS.IgnoreUserEnteredSignals() 309 | // This allows to gracefully close the process and execute all defers 310 | signal.Ignore(syscall.SIGHUP) 311 | defer signal.Reset() 312 | err := shell.Run() 313 | if err != nil { 314 | return err 315 | } 316 | return nil 317 | } 318 | -------------------------------------------------------------------------------- /pkg/aws/aws_test.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | // Helpers Mock generation 4 | //go:generate go run github.com/golang/mock/mockgen -self_package=github.com/danmx/sigil/pkg/aws -package aws -destination helpers_mock_test.go github.com/danmx/sigil/pkg/aws/helpers OSExecIface,OSIface 5 | // AWS EC2 Mock generation 6 | //go:generate go run github.com/golang/mock/mockgen -self_package=github.com/danmx/sigil/pkg/aws -package aws -destination ec2_aws_mock_test.go github.com/aws/aws-sdk-go/service/ec2/ec2iface EC2API 7 | 8 | import ( 9 | "errors" 10 | "testing" 11 | 12 | awsCloud "github.com/aws/aws-sdk-go/aws" 13 | _ "github.com/aws/aws-sdk-go/aws/request" // for mocking EC2API 14 | "github.com/aws/aws-sdk-go/service/ec2" 15 | "github.com/golang/mock/gomock" 16 | "github.com/stretchr/testify/assert" 17 | ) 18 | 19 | // TestVerifyDependencies verifies the dependency verifier 20 | func TestVerifyDependencies(t *testing.T) { 21 | ctrl := gomock.NewController(t) 22 | defer ctrl.Finish() 23 | m := NewMockOSExecIface(ctrl) // skipcq: SCC-compile 24 | 25 | gomock.InOrder( 26 | m.EXPECT().LookPath(pluginName).Return("", errors.New("")), 27 | ) 28 | 29 | assert.Error(t, verifyDependencies(m)) 30 | 31 | gomock.InOrder( 32 | m.EXPECT().LookPath(pluginName).Return("/usr/local/bin/session-manager-plugin", nil), 33 | ) 34 | 35 | assert.NoError(t, verifyDependencies(m)) 36 | } 37 | 38 | // TestAppendUserAgent verifies env var was set 39 | func TestAppendUserAgent(t *testing.T) { 40 | ctrl := gomock.NewController(t) 41 | defer ctrl.Finish() 42 | m := NewMockOSIface(ctrl) // skipcq: SCC-compile 43 | 44 | suffix := "test" 45 | 46 | gomock.InOrder( 47 | m.EXPECT().LookupEnv(execEnvVar).Return("", false), 48 | m.EXPECT().Setenv(execEnvVar, suffix).Return(nil), 49 | ) 50 | 51 | assert.NoError(t, appendUserAgent(m, suffix)) 52 | 53 | gomock.InOrder( 54 | m.EXPECT().LookupEnv(execEnvVar).Return("value", true), 55 | m.EXPECT().Setenv(execEnvVar, "value/"+suffix).Return(nil), 56 | ) 57 | 58 | assert.NoError(t, appendUserAgent(m, suffix)) 59 | } 60 | 61 | // TestGetFilters tests fetching instance filters 62 | func TestGetFilters(t *testing.T) { 63 | target := "testTarget" 64 | testMap := map[string][]*ec2.Filter{ 65 | TargetTypeInstanceID: { 66 | { 67 | Name: awsCloud.String("instance-id"), 68 | Values: []*string{&target}, 69 | }, 70 | { 71 | Name: awsCloud.String("instance-state-name"), 72 | Values: []*string{awsCloud.String("running")}, 73 | }, 74 | }, 75 | TargetTypePrivateDNS: { 76 | { 77 | Name: awsCloud.String("private-dns-name"), 78 | Values: []*string{&target}, 79 | }, 80 | { 81 | Name: awsCloud.String("instance-state-name"), 82 | Values: []*string{awsCloud.String("running")}, 83 | }, 84 | }, 85 | TargetTypeName: { 86 | { 87 | Name: awsCloud.String("tag:Name"), 88 | Values: []*string{&target}, 89 | }, 90 | { 91 | Name: awsCloud.String("instance-state-name"), 92 | Values: []*string{awsCloud.String("running")}, 93 | }, 94 | }, 95 | } 96 | for key, value := range testMap { 97 | filter, err := getFilters(key, target) 98 | assert.Equal(t, filter, value) 99 | assert.NoError(t, err) 100 | } 101 | _, err := getFilters("invalid", target) 102 | assert.Error(t, err) 103 | } 104 | 105 | // TestGetFirstInstance tests listing the first instance 106 | func TestGetFirstInstance(t *testing.T) { 107 | ctrl := gomock.NewController(t) 108 | defer ctrl.Finish() 109 | m := NewMockEC2API(ctrl) // skipcq: SCC-compile 110 | 111 | filters := []*ec2.Filter{} 112 | 113 | input := &ec2.DescribeInstancesInput{ 114 | Filters: filters, 115 | MaxResults: awsCloud.Int64(maxResults), 116 | } 117 | 118 | gomock.InOrder( 119 | m.EXPECT().DescribeInstancesPages(input, gomock.Any()).Return(nil), 120 | m.EXPECT().DescribeInstancesPages(input, gomock.Any()).Return(errors.New("")), 121 | ) 122 | _, err := getFirstInstance(m, filters) 123 | assert.NoError(t, err) 124 | _, err = getFirstInstance(m, filters) 125 | assert.Error(t, err) 126 | } 127 | -------------------------------------------------------------------------------- /pkg/aws/helpers/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = ["helpers.go"], 6 | importpath = "github.com/danmx/sigil/pkg/aws/helpers", 7 | visibility = ["//visibility:public"], 8 | ) 9 | -------------------------------------------------------------------------------- /pkg/aws/helpers/helpers.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | ) 7 | 8 | type OSExecIface interface { 9 | LookPath(file string) (string, error) 10 | } 11 | 12 | type OSIface interface { 13 | LookupEnv(envVar string) (string, bool) 14 | Setenv(envVar, value string) error 15 | } 16 | 17 | type Helpers struct{} 18 | 19 | // LookupEnv looks up an environment variable 20 | func (Helpers) LookupEnv(envVar string) (string, bool) { 21 | return os.LookupEnv(envVar) 22 | } 23 | 24 | // Setenv sets an environment variable 25 | func (Helpers) Setenv(envVar, value string) error { 26 | return os.Setenv(envVar, value) 27 | } 28 | 29 | // LookPath looks up a path 30 | func (Helpers) LookPath(file string) (string, error) { 31 | return exec.LookPath(file) 32 | } 33 | -------------------------------------------------------------------------------- /pkg/aws/interface.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | // Cloud wraps init methods used from the aws package 4 | type Cloud interface { 5 | NewWithConfig(c *Config) error 6 | } 7 | 8 | // CloudInstances wraps instances methods used from the aws package 9 | type CloudInstances interface { 10 | Cloud 11 | ListInstances() ([]*Instance, error) 12 | StartSession(targetType, target string) error 13 | } 14 | 15 | // CloudSessions wraps sessions methods used from the aws package 16 | type CloudSessions interface { 17 | Cloud 18 | ListSessions() ([]*Session, error) 19 | TerminateSession(sessionID string) error 20 | } 21 | 22 | // CloudSSH wraps ssh methods used from the aws package 23 | type CloudSSH interface { 24 | Cloud 25 | StartSSH(targetType, target, osUser string, portNumber uint64, publicKey []byte) error 26 | } 27 | -------------------------------------------------------------------------------- /pkg/aws/list.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws" 5 | "github.com/aws/aws-sdk-go/service/ec2" 6 | "github.com/aws/aws-sdk-go/service/ec2/ec2iface" 7 | "github.com/aws/aws-sdk-go/service/ssm" 8 | "github.com/aws/aws-sdk-go/service/ssm/ssmiface" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | const capMultiplier = 2 13 | 14 | // ListInstances provides a list of active instances with SSM agent 15 | func (p *Provider) ListInstances() ([]*Instance, error) { 16 | instances, err := fetchInstances(ssm.New(p.session), p.filters.Instance.IDs) 17 | if err != nil { 18 | return nil, err 19 | } 20 | if len(instances) == 0 { 21 | log.Info("No matching instances") 22 | return []*Instance{}, nil 23 | } 24 | filteredInstances, err := filterInstances(ec2.New(p.session), p.filters.Instance.Tags, instances) 25 | if err != nil { 26 | return nil, err 27 | } 28 | return filteredInstances, nil 29 | } 30 | 31 | func fetchInstances(ssmClient ssmiface.SSMAPI, ids []string) (map[string]*Instance, error) { 32 | // Show only instances that have active agents 33 | ssmDescribeInstancesInput := &ssm.DescribeInstanceInformationInput{ 34 | MaxResults: aws.Int64(maxResults), 35 | Filters: []*ssm.InstanceInformationStringFilter{ 36 | { 37 | Key: aws.String("PingStatus"), 38 | Values: []*string{aws.String("Online")}, 39 | }, 40 | }, 41 | } 42 | if len(ids) > 0 { 43 | log.WithFields(log.Fields{ 44 | "ids": ids, 45 | }).Debug("Instance IDs Filter") 46 | ssmDescribeInstancesInput.Filters = append(ssmDescribeInstancesInput.Filters, &ssm.InstanceInformationStringFilter{ 47 | Key: aws.String("InstanceIds"), 48 | Values: aws.StringSlice(ids), 49 | }) 50 | } 51 | 52 | instances := make(map[string]*Instance) 53 | 54 | err := ssmClient.DescribeInstanceInformationPages(ssmDescribeInstancesInput, 55 | func(page *ssm.DescribeInstanceInformationOutput, lastPage bool) bool { 56 | for _, instance := range page.InstanceInformationList { 57 | log.WithFields(log.Fields{ 58 | "InstanceId": *instance.InstanceId, 59 | "ComputerName": *instance.ComputerName, 60 | "IPAddress": *instance.IPAddress, 61 | "PlatformName": *instance.PlatformName, 62 | "PlatformType": *instance.PlatformType, 63 | "PlatformVersion": *instance.PlatformVersion, 64 | }).Debug("Describe Instance") 65 | instances[*instance.InstanceId] = &Instance{ 66 | Hostname: *instance.ComputerName, 67 | IPAddress: *instance.IPAddress, 68 | ID: *instance.InstanceId, 69 | OSName: *instance.PlatformName, 70 | OSType: *instance.PlatformType, 71 | OSVersion: *instance.PlatformVersion, 72 | } 73 | } 74 | return !lastPage 75 | }) 76 | if err != nil { 77 | log.WithFields(log.Fields{ 78 | "input": *ssmDescribeInstancesInput, 79 | "error": err, 80 | }).Error("DescribeInstanceInformationPages") 81 | return nil, err 82 | } 83 | return instances, nil 84 | } 85 | 86 | func filterInstances(ec2Client ec2iface.EC2API, tags []TagValues, instances map[string]*Instance) ([]*Instance, error) { 87 | describeInstancesInput := &ec2.DescribeInstancesInput{ 88 | InstanceIds: make([]*string, 0, len(instances)), 89 | Filters: []*ec2.Filter{ 90 | { 91 | Name: aws.String("instance-state-name"), 92 | Values: []*string{aws.String("running")}, 93 | }, 94 | }, 95 | } 96 | 97 | for _, tag := range tags { 98 | log.WithFields(log.Fields{ 99 | "key": tag.Key, 100 | "values": tag.Values, 101 | }).Debug("Tags Filter") 102 | describeInstancesInput.Filters = append(describeInstancesInput.Filters, &ec2.Filter{ 103 | Name: aws.String("tag:" + tag.Key), 104 | Values: aws.StringSlice(tag.Values), 105 | }) 106 | } 107 | 108 | outputInstances := make([]*Instance, 0, len(instances)) 109 | // Discovering instances private DNS names 110 | for _, instance := range instances { 111 | describeInstancesInput.InstanceIds = append(describeInstancesInput.InstanceIds, &instance.ID) 112 | } 113 | err := ec2Client.DescribeInstancesPages(describeInstancesInput, 114 | func(page *ec2.DescribeInstancesOutput, lastPage bool) bool { 115 | for _, reservation := range page.Reservations { 116 | for _, instance := range reservation.Instances { 117 | nameTag := "" 118 | for _, tag := range instance.Tags { 119 | if *tag.Key == "Name" { 120 | nameTag = *tag.Value 121 | break 122 | } 123 | } 124 | instances[*instance.InstanceId].PrivateDNSName = *instance.PrivateDnsName 125 | instances[*instance.InstanceId].Name = nameTag 126 | outputInstances = append(outputInstances, instances[*instance.InstanceId]) 127 | } 128 | } 129 | return !lastPage 130 | }) 131 | if err != nil { 132 | log.WithFields(log.Fields{ 133 | "input": *describeInstancesInput, 134 | "error": err, 135 | }).Error("DescribeInstancesPages") 136 | return nil, err 137 | } 138 | return outputInstances, nil 139 | } 140 | 141 | // ListSessions provides a list of active SSM sessions 142 | func (p *Provider) ListSessions() ([]*Session, error) { 143 | return listSessions(ssm.New(p.session), p.filters.Session) 144 | } 145 | 146 | func listSessions(ssmClient ssmiface.SSMAPI, sessionFilters SessionFilters) ([]*Session, error) { 147 | // Show only connected sessions 148 | ssmDescribeSessionsInput := &ssm.DescribeSessionsInput{ 149 | State: aws.String("Active"), 150 | } 151 | // Parse filters 152 | filters := []*ssm.SessionFilter{} 153 | if sessionFilters.After != "" { 154 | filters = append(filters, &ssm.SessionFilter{ 155 | Key: aws.String("InvokedAfter"), 156 | Value: &sessionFilters.After, 157 | }) 158 | } 159 | if sessionFilters.Before != "" { 160 | filters = append(filters, &ssm.SessionFilter{ 161 | Key: aws.String("InvokedBefore"), 162 | Value: &sessionFilters.Before, 163 | }) 164 | } 165 | if sessionFilters.Target != "" { 166 | filters = append(filters, &ssm.SessionFilter{ 167 | Key: aws.String("Target"), 168 | Value: &sessionFilters.Target, 169 | }) 170 | } 171 | if sessionFilters.Owner != "" { 172 | filters = append(filters, &ssm.SessionFilter{ 173 | Key: aws.String("Owner"), 174 | Value: &sessionFilters.Owner, 175 | }) 176 | } 177 | if len(filters) > 0 { 178 | ssmDescribeSessionsInput.Filters = filters 179 | } 180 | sessions := []*Session{} 181 | for out, err := ssmClient.DescribeSessions(ssmDescribeSessionsInput); ; { 182 | if err != nil { 183 | log.WithField("error", err).Error("DescribeSessions") 184 | return nil, err 185 | } 186 | log.WithField("sessions array len", len(out.Sessions)).Debug("Sessions Output") 187 | if len(sessions)+1 > cap(sessions) { 188 | newSlice := make([]*Session, len(sessions), (cap(sessions))*capMultiplier) 189 | n := copy(newSlice, sessions) 190 | log.WithField("no. copied elements", n).Debug("Expand Sessions slice") 191 | sessions = newSlice 192 | } 193 | for i, sess := range out.Sessions { 194 | log.WithField("session", sess).Debugf("Single session #%d", i) 195 | startDate, err := sess.StartDate.MarshalText() 196 | if err != nil { 197 | log.WithField("error", err).Error("StartDate MarshalText") 198 | return nil, err 199 | } 200 | startDateString := string(startDate) 201 | sessions = append(sessions, &Session{ 202 | SessionID: *sess.SessionId, 203 | Target: *sess.Target, 204 | Status: *sess.Status, 205 | StartDate: startDateString, 206 | Owner: *sess.Owner, 207 | }) 208 | } 209 | if out.NextToken == nil { 210 | break 211 | } 212 | ssmDescribeSessionsInput.NextToken = out.NextToken 213 | } 214 | return sessions, nil 215 | } 216 | -------------------------------------------------------------------------------- /pkg/aws/list_test.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | // AWS SSM Mock generation 4 | //go:generate go run github.com/golang/mock/mockgen -self_package=github.com/danmx/sigil/pkg/aws -package aws -destination ssm_aws_mock_test.go github.com/aws/aws-sdk-go/service/ssm/ssmiface SSMAPI 5 | // AWS EC2 Mock generation 6 | //go:generate go run github.com/golang/mock/mockgen -self_package=github.com/danmx/sigil/pkg/aws -package aws -destination ec2_aws_mock_test.go github.com/aws/aws-sdk-go/service/ec2/ec2iface EC2API 7 | 8 | import ( 9 | "testing" 10 | 11 | "github.com/aws/aws-sdk-go/aws" 12 | "github.com/aws/aws-sdk-go/service/ec2" 13 | "github.com/aws/aws-sdk-go/service/ssm" 14 | "github.com/golang/mock/gomock" 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | // TestFetchInstances verifies instances fetching 19 | func TestFetchInstances(t *testing.T) { 20 | ctrl := gomock.NewController(t) 21 | defer ctrl.Finish() 22 | m := NewMockSSMAPI(ctrl) // skipcq: SCC-compile 23 | 24 | IDs := []string{"1", "2", "3"} 25 | 26 | instances := make(map[string]*Instance) 27 | 28 | input := &ssm.DescribeInstanceInformationInput{ 29 | MaxResults: aws.Int64(maxResults), 30 | Filters: []*ssm.InstanceInformationStringFilter{ 31 | { 32 | Key: aws.String("PingStatus"), 33 | Values: []*string{aws.String("Online")}, 34 | }, 35 | { 36 | Key: aws.String("InstanceIds"), 37 | Values: aws.StringSlice(IDs), 38 | }, 39 | }, 40 | } 41 | 42 | gomock.InOrder( 43 | m.EXPECT().DescribeInstanceInformationPages(input, gomock.Any()).Return(nil), 44 | ) 45 | list, err := fetchInstances(m, IDs) 46 | assert.Equal(t, instances, list) 47 | assert.NoError(t, err) 48 | } 49 | 50 | // TestFilterInstances verifies instances filtering 51 | func TestFilterInstances(t *testing.T) { 52 | ctrl := gomock.NewController(t) 53 | defer ctrl.Finish() 54 | m := NewMockEC2API(ctrl) // skipcq: SCC-compile 55 | 56 | tags := []TagValues{ 57 | { 58 | Key: "TestKey1", 59 | Values: []string{"1"}, 60 | }, 61 | } 62 | 63 | instances := make(map[string]*Instance) 64 | 65 | outputInstances := []*Instance{} 66 | 67 | input := &ec2.DescribeInstancesInput{ 68 | InstanceIds: make([]*string, 0, len(instances)), 69 | Filters: []*ec2.Filter{ 70 | { 71 | Name: aws.String("instance-state-name"), 72 | Values: []*string{aws.String("running")}, 73 | }, 74 | }, 75 | } 76 | for _, tag := range tags { 77 | input.Filters = append(input.Filters, &ec2.Filter{ 78 | Name: aws.String("tag:" + tag.Key), 79 | Values: aws.StringSlice(tag.Values), 80 | }) 81 | } 82 | for _, instance := range instances { 83 | input.InstanceIds = append(input.InstanceIds, &instance.ID) 84 | } 85 | 86 | gomock.InOrder( 87 | m.EXPECT().DescribeInstancesPages(input, gomock.Any()).Return(nil), 88 | ) 89 | list, err := filterInstances(m, tags, instances) 90 | assert.Equal(t, outputInstances, list) 91 | assert.NoError(t, err) 92 | } 93 | 94 | // TestListSessions verifies session listing 95 | func TestListSessions(t *testing.T) { 96 | ctrl := gomock.NewController(t) 97 | defer ctrl.Finish() 98 | m := NewMockSSMAPI(ctrl) // skipcq: SCC-compile 99 | 100 | filters := SessionFilters{ 101 | After: "after", 102 | Before: "before", 103 | Target: "target", 104 | Owner: "owner", 105 | } 106 | 107 | sessions := []*Session{} 108 | 109 | input := &ssm.DescribeSessionsInput{ 110 | State: aws.String("Active"), 111 | Filters: []*ssm.SessionFilter{ 112 | { 113 | Key: aws.String("InvokedAfter"), 114 | Value: &filters.After, 115 | }, 116 | { 117 | Key: aws.String("InvokedBefore"), 118 | Value: &filters.Before, 119 | }, 120 | { 121 | Key: aws.String("Target"), 122 | Value: &filters.Target, 123 | }, 124 | { 125 | Key: aws.String("Owner"), 126 | Value: &filters.Owner, 127 | }, 128 | }, 129 | } 130 | 131 | output := &ssm.DescribeSessionsOutput{ 132 | NextToken: nil, 133 | Sessions: []*ssm.Session{}, 134 | } 135 | 136 | gomock.InOrder( 137 | m.EXPECT().DescribeSessions(input).Return(output, nil), 138 | ) 139 | 140 | list, err := listSessions(m, filters) 141 | assert.Equal(t, sessions, list) 142 | assert.NoError(t, err) 143 | } 144 | -------------------------------------------------------------------------------- /pkg/aws/log/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = ["log.go"], 6 | importpath = "github.com/danmx/sigil/pkg/aws/log", 7 | visibility = ["//visibility:public"], 8 | deps = [ 9 | "@com_github_aws_aws_sdk_go//aws:go_default_library", 10 | "@com_github_sirupsen_logrus//:go_default_library", 11 | ], 12 | ) 13 | -------------------------------------------------------------------------------- /pkg/aws/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws" 5 | logr "github.com/sirupsen/logrus" 6 | ) 7 | 8 | // A traceLogger provides a minimalistic logger satisfying the aws.Logger interface. 9 | type traceLogger struct{} 10 | 11 | // newTraceLogger returns a Logger which will write log messages to current logger 12 | func NewTraceLogger() aws.Logger { 13 | return &traceLogger{} 14 | } 15 | 16 | // Log logs the parameters to the stdlib logger. See log.Println. 17 | func (traceLogger) Log(args ...interface{}) { 18 | logr.Trace(args...) 19 | } 20 | -------------------------------------------------------------------------------- /pkg/aws/session.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/aws/aws-sdk-go/service/ssm" 8 | "github.com/aws/aws-sdk-go/service/ssm/ssmiface" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // StartSession will start a session for a chosed node 13 | func (p *Provider) StartSession(targetType, target string) error { 14 | instance, err := p.getInstance(targetType, target) 15 | if err != nil { 16 | log.WithFields(log.Fields{ 17 | "targetType": targetType, 18 | "target": target, 19 | }).Error("failed getting the instance") 20 | return err 21 | } 22 | log.WithField("target instance id", *instance.InstanceId).Debug("Checking the target instance ID") 23 | startSessionInput := &ssm.StartSessionInput{ 24 | Target: instance.InstanceId, 25 | } 26 | ssmClient := ssm.New(p.session) 27 | output, err := ssmClient.StartSession(startSessionInput) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | defer func() { 33 | if err = terminateSession(ssmClient, *output.SessionId); err != nil { 34 | err = fmt.Errorf("failed terminating the session (it could be already terminated): %e", err) 35 | log.Warn(err) 36 | } 37 | }() 38 | 39 | log.WithFields(log.Fields{ 40 | "sessionID": *output.SessionId, 41 | "streamURL": *output.StreamUrl, 42 | "token": *output.TokenValue, 43 | }).Debug("SSM Start Session Output") 44 | payload, err := json.Marshal(output) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | startSessionInputJSON, err := json.Marshal(startSessionInput) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | return runSessionPluginManager(string(payload), *p.session.Config.Region, p.awsProfile, string(startSessionInputJSON), ssmClient.Client.Endpoint) 55 | } 56 | 57 | // TerminateSession will close chosed active session 58 | func (p *Provider) TerminateSession(sessionID string) error { 59 | return terminateSession(ssm.New(p.session), sessionID) 60 | } 61 | 62 | func terminateSession(ssmClient ssmiface.SSMAPI, sessionID string) error { 63 | _, err := ssmClient.TerminateSession(&ssm.TerminateSessionInput{ 64 | SessionId: &sessionID, 65 | }) 66 | if err != nil { 67 | log.WithFields(log.Fields{"sessionID": sessionID}).Warn(err) 68 | return err 69 | } 70 | return nil 71 | } 72 | -------------------------------------------------------------------------------- /pkg/aws/session_test.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | // AWS SSM Mock generation 4 | //go:generate go run github.com/golang/mock/mockgen -self_package=github.com/danmx/sigil/pkg/aws -package aws -destination ssm_aws_mock_test.go github.com/aws/aws-sdk-go/service/ssm/ssmiface SSMAPI 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/aws/aws-sdk-go/service/ssm" 10 | "github.com/golang/mock/gomock" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | // TestTerminateSession verifies session termination 15 | func TestTerminateSession(t *testing.T) { 16 | ctrl := gomock.NewController(t) 17 | defer ctrl.Finish() 18 | m := NewMockSSMAPI(ctrl) // skipcq: SCC-compile 19 | 20 | sessionID := "testID" 21 | 22 | gomock.InOrder( 23 | m.EXPECT().TerminateSession(&ssm.TerminateSessionInput{ 24 | SessionId: &sessionID, 25 | }).Return(new(ssm.TerminateSessionOutput), nil), 26 | ) 27 | 28 | assert.NoError(t, terminateSession(m, sessionID)) 29 | } 30 | -------------------------------------------------------------------------------- /pkg/aws/ssh.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strconv" 7 | 8 | "github.com/aws/aws-sdk-go/aws" 9 | "github.com/aws/aws-sdk-go/service/ec2instanceconnect" 10 | "github.com/aws/aws-sdk-go/service/ec2instanceconnect/ec2instanceconnectiface" 11 | "github.com/aws/aws-sdk-go/service/ssm" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | // StartSSH will start a ssh proxy session for a chosed node 16 | func (p *Provider) StartSSH(targetType, target, osUser string, portNumber uint64, publicKey []byte) error { 17 | instance, err := p.getInstance(targetType, target) 18 | if err != nil { 19 | return err 20 | } 21 | 22 | if len(publicKey) > 0 { 23 | svc := ec2instanceconnect.New(p.session) 24 | err = uploadPublicKey(svc, publicKey, osUser, *instance.InstanceId, *instance.Placement.AvailabilityZone) 25 | if err != nil { 26 | return err 27 | } 28 | } 29 | 30 | ssmClient := ssm.New(p.session) 31 | parameters := map[string][]*string{ 32 | "portNumber": {aws.String(strconv.FormatUint(portNumber, 10))}, //nolint:gomnd // decimal base 33 | } 34 | startSessionInput := &ssm.StartSessionInput{ 35 | Parameters: parameters, 36 | Target: instance.InstanceId, 37 | DocumentName: aws.String("AWS-StartSSHSession"), 38 | } 39 | output, err := ssmClient.StartSession(startSessionInput) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | defer func() { 45 | if err = terminateSession(ssmClient, *output.SessionId); err != nil { 46 | err = fmt.Errorf("failed terminating the session (it could be already terminated): %e", err) 47 | log.Warn(err) 48 | } 49 | }() 50 | 51 | log.WithFields(log.Fields{ 52 | "sessionID": *output.SessionId, 53 | "streamURL": *output.StreamUrl, 54 | "token": *output.TokenValue, 55 | }).Debug("SSM Start Session Output") 56 | payload, err := json.Marshal(output) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | startSessionInputJSON, err := json.Marshal(startSessionInput) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | endpoint := ssmClient.Client.Endpoint 67 | 68 | return runSessionPluginManager(string(payload), *p.session.Config.Region, p.awsProfile, string(startSessionInputJSON), endpoint) 69 | } 70 | 71 | func uploadPublicKey(client ec2instanceconnectiface.EC2InstanceConnectAPI, publicKey []byte, osUser, instanceID, availabilityZone string) error { 72 | pubKey := string(publicKey) 73 | log.WithFields(log.Fields{ 74 | "SSHPublicKey": pubKey, 75 | "InstanceOSUser": osUser, 76 | "InstanceId": instanceID, 77 | "AvailabilityZone": availabilityZone, 78 | }).Debug("SendSSHPublicKey") 79 | 80 | out, err := client.SendSSHPublicKey(&ec2instanceconnect.SendSSHPublicKeyInput{ 81 | AvailabilityZone: &availabilityZone, 82 | InstanceId: &instanceID, 83 | InstanceOSUser: &osUser, 84 | SSHPublicKey: &pubKey, 85 | }) 86 | if err != nil { 87 | log.WithFields(log.Fields{ 88 | "AvailabilityZone": availabilityZone, 89 | "InstanceID": instanceID, 90 | "InstanceOSUser": osUser, 91 | "SSHPublicKey": pubKey, 92 | "error": err, 93 | }).Error("failed SendSSHPublicKey") 94 | return err 95 | } 96 | if !*out.Success { 97 | return fmt.Errorf("failed SendSSHPublicKey. RequestID: %s", *out.RequestId) 98 | } 99 | return nil 100 | } 101 | -------------------------------------------------------------------------------- /pkg/aws/ssh_test.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | // AWS EC2 Instance Connect Mock generation 4 | //go:generate go run github.com/golang/mock/mockgen -self_package=github.com/danmx/sigil/pkg/aws -package aws -destination ec2instanceconnect_aws_mock_test.go github.com/aws/aws-sdk-go/service/ec2instanceconnect/ec2instanceconnectiface EC2InstanceConnectAPI 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/aws/aws-sdk-go/aws" 10 | "github.com/aws/aws-sdk-go/service/ec2instanceconnect" 11 | "github.com/golang/mock/gomock" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | // TestUploadPublicKey verifies upload of a public key 16 | func TestUploadPublicKey(t *testing.T) { 17 | ctrl := gomock.NewController(t) 18 | defer ctrl.Finish() 19 | m := NewMockEC2InstanceConnectAPI(ctrl) // skipcq: SCC-compile 20 | 21 | availabilityZone := "a" 22 | instanceID := "1" 23 | osUser := "ec2user" 24 | pubKey := "testKey" 25 | 26 | output := &ec2instanceconnect.SendSSHPublicKeyOutput{ 27 | RequestId: aws.String("testSuccess"), 28 | Success: aws.Bool(true), 29 | } 30 | 31 | gomock.InOrder( 32 | m.EXPECT().SendSSHPublicKey(&ec2instanceconnect.SendSSHPublicKeyInput{ 33 | AvailabilityZone: &availabilityZone, 34 | InstanceId: &instanceID, 35 | InstanceOSUser: &osUser, 36 | SSHPublicKey: &pubKey, 37 | }).Return(output, nil), 38 | ) 39 | 40 | assert.NoError(t, uploadPublicKey(m, []byte(pubKey), osUser, instanceID, availabilityZone)) 41 | 42 | output = &ec2instanceconnect.SendSSHPublicKeyOutput{ 43 | Success: aws.Bool(false), 44 | RequestId: aws.String("testFailure"), 45 | } 46 | 47 | gomock.InOrder( 48 | m.EXPECT().SendSSHPublicKey(&ec2instanceconnect.SendSSHPublicKeyInput{ 49 | AvailabilityZone: &availabilityZone, 50 | InstanceId: &instanceID, 51 | InstanceOSUser: &osUser, 52 | SSHPublicKey: &pubKey, 53 | }).Return(output, nil), 54 | ) 55 | 56 | assert.Error(t, uploadPublicKey(m, []byte(pubKey), osUser, instanceID, availabilityZone)) 57 | } 58 | -------------------------------------------------------------------------------- /pkg/list/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") 2 | load("@com_github_jmhodges_bazel_gomock//:gomock.bzl", "gomock") 3 | 4 | go_library( 5 | name = "go_default_library", 6 | srcs = ["list.go"], 7 | importpath = "github.com/danmx/sigil/pkg/list", 8 | visibility = ["//visibility:public"], 9 | deps = [ 10 | "//pkg/aws:go_default_library", 11 | "@com_github_sirupsen_logrus//:go_default_library", 12 | "@in_gopkg_yaml_v2//:go_default_library", 13 | ], 14 | ) 15 | 16 | go_test( 17 | name = "go_default_test", 18 | srcs = [ 19 | "aws_mock_test.go", 20 | "list_test.go", 21 | ], 22 | embed = [":go_default_library"], 23 | deps = [ 24 | "//pkg/aws:go_default_library", 25 | "@com_github_golang_mock//gomock:go_default_library", 26 | "@com_github_stretchr_testify//assert:go_default_library", 27 | ], 28 | ) 29 | 30 | gomock( 31 | name = "mock_aws", 32 | out = "aws_mock_test.go", 33 | interfaces = [ 34 | "Cloud", 35 | "CloudInstances", 36 | "CloudSessions", 37 | "CloudSSH", 38 | ], 39 | library = "//pkg/aws:go_default_library", 40 | package = "list", 41 | self_package = "github.com/danmx/sigil/pkg/list", 42 | ) 43 | -------------------------------------------------------------------------------- /pkg/list/list.go: -------------------------------------------------------------------------------- 1 | package list 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/json" 7 | "fmt" 8 | "os" 9 | "strconv" 10 | "strings" 11 | "text/tabwriter" 12 | 13 | "github.com/danmx/sigil/pkg/aws" 14 | 15 | log "github.com/sirupsen/logrus" 16 | yaml "gopkg.in/yaml.v2" 17 | ) 18 | 19 | // List wraps methods used from the pkg/list package 20 | type List interface { 21 | Start(input *StartInput) error 22 | } 23 | 24 | const ( 25 | // FormatText points to the text format type 26 | FormatText = "text" 27 | // FormatWide points to the wide format type 28 | FormatWide = "wide" 29 | // FormatJSON points to the json format type 30 | FormatJSON = "json" 31 | // FormatYAML points to the yaml format type 32 | FormatYAML = "yaml" 33 | // TypeListInstances points to the instances list type 34 | TypeListInstances = "instances" 35 | // TypeListSessions points to the sessions list type 36 | TypeListSessions = "sessions" 37 | tabPadding = 2 38 | ) 39 | 40 | // StartInput struct contains all input data 41 | type StartInput struct { 42 | // Define output format 43 | OutputFormat *string 44 | Interactive *bool 45 | Type *string 46 | MFAToken *string 47 | Region *string 48 | Profile *string 49 | Filters *aws.Filters 50 | Trace *bool 51 | } 52 | 53 | // Start will output a ist of all available EC2 instances 54 | func Start(input *StartInput) error { 55 | provider := aws.Provider{} 56 | err := provider.NewWithConfig(&aws.Config{ 57 | Filters: *input.Filters, 58 | Region: *input.Region, 59 | Profile: *input.Profile, 60 | MFAToken: *input.MFAToken, 61 | Trace: *input.Trace, 62 | }) 63 | if err != nil { 64 | log.Error(err) 65 | return err 66 | } 67 | switch *input.Type { 68 | case TypeListInstances: 69 | err := input.listInstances(&provider) 70 | if err != nil { 71 | return err 72 | } 73 | case TypeListSessions: 74 | err := input.listSessions(&provider) 75 | if err != nil { 76 | return err 77 | } 78 | default: 79 | err := fmt.Errorf("unsupported list type: %s", *input.Type) 80 | log.WithField("type", *input.Type).Error(err) 81 | return err 82 | } 83 | return nil 84 | } 85 | 86 | func sessionsToString(format string, sessions []*aws.Session) (string, error) { 87 | switch format { 88 | case FormatText: 89 | buf := bytes.NewBufferString("") 90 | w := new(tabwriter.Writer) 91 | w.Init(buf, 0, 0, tabPadding, ' ', 0) 92 | fmt.Fprintln(w, "Index\tSession ID\tTarget\tStart Date") 93 | for i, session := range sessions { 94 | fmt.Fprintf(w, "%d\t%s\t%s\t%s\n", 95 | (i + 1), session.SessionID, session.Target, session.StartDate) 96 | } 97 | err := w.Flush() 98 | if err != nil { 99 | return "", err 100 | } 101 | return buf.String(), nil 102 | case FormatJSON: 103 | data, err := json.Marshal(sessions) 104 | if err != nil { 105 | return "", err 106 | } 107 | // JSON output was missing new line 108 | return string(data) + "\n", nil 109 | case FormatYAML: 110 | data, err := yaml.Marshal(sessions) 111 | if err != nil { 112 | return "", err 113 | } 114 | return string(data), nil 115 | case FormatWide: 116 | buf := new(bytes.Buffer) 117 | w := new(tabwriter.Writer) 118 | w.Init(buf, 0, 0, tabPadding, ' ', 0) 119 | fmt.Fprintln(w, "Index\tSession ID\tTarget\tStart Date\tOwner\tStatus") 120 | for i, session := range sessions { 121 | fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%s\n", 122 | (i + 1), session.SessionID, session.Target, session.StartDate, 123 | session.Owner, session.Status) 124 | } 125 | err := w.Flush() 126 | if err != nil { 127 | return "", err 128 | } 129 | return buf.String(), nil 130 | default: 131 | return "", fmt.Errorf("unsupported output format: %s", format) 132 | } 133 | } 134 | 135 | func instancesToString(format string, instances []*aws.Instance) (string, error) { 136 | switch format { 137 | case FormatText: 138 | buf := bytes.NewBufferString("") 139 | w := new(tabwriter.Writer) 140 | w.Init(buf, 0, 0, tabPadding, ' ', 0) 141 | fmt.Fprintln(w, "Index\tName\tInstance ID\tIP Address\tPrivate DNS Name") 142 | for i, instance := range instances { 143 | fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\n", 144 | (i + 1), instance.Name, instance.ID, instance.IPAddress, instance.PrivateDNSName) 145 | } 146 | err := w.Flush() 147 | if err != nil { 148 | return "", err 149 | } 150 | return buf.String(), nil 151 | case FormatJSON: 152 | data, err := json.Marshal(instances) 153 | if err != nil { 154 | return "", err 155 | } 156 | // JSON output was missing new line 157 | return string(data) + "\n", nil 158 | case FormatYAML: 159 | data, err := yaml.Marshal(instances) 160 | if err != nil { 161 | return "", err 162 | } 163 | return string(data), nil 164 | case FormatWide: 165 | buf := new(bytes.Buffer) 166 | w := new(tabwriter.Writer) 167 | w.Init(buf, 0, 0, tabPadding, ' ', 0) 168 | fmt.Fprintln(w, "Index\tName\tInstance ID\tIP Address\tPrivate DNS Name\tHostname\tOS Name\tOS Version\tOS Type") 169 | for i, instance := range instances { 170 | fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", 171 | (i + 1), instance.Name, instance.ID, instance.IPAddress, instance.PrivateDNSName, 172 | instance.Hostname, instance.OSName, instance.OSVersion, instance.OSType) 173 | } 174 | err := w.Flush() 175 | if err != nil { 176 | return "", err 177 | } 178 | return buf.String(), nil 179 | default: 180 | return "", fmt.Errorf("unsupported output format: %s", format) 181 | } 182 | } 183 | 184 | func (input *StartInput) listInstances(provider aws.CloudInstances) error { 185 | instances, err := provider.ListInstances() 186 | if err != nil { 187 | log.Error("Failed listing instances") 188 | return err 189 | } 190 | outString, err := instancesToString(*input.OutputFormat, instances) 191 | if err != nil { 192 | log.Error("Failed stringifying instances") 193 | return err 194 | } 195 | // TODO Mock stdout 196 | fmt.Fprint(os.Stdout, outString) 197 | if *input.Interactive && len(instances) > 0 { 198 | // TODO Mock stdin and stderr 199 | reader := bufio.NewReader(os.Stdin) 200 | fmt.Fprintf(os.Stderr, "Choose an instance to connect to [1 - %d]: ", len(instances)) 201 | textInput, err := reader.ReadString('\n') 202 | if err != nil { 203 | return err 204 | } 205 | i, err := strconv.Atoi(strings.ReplaceAll(textInput, "\n", "")) 206 | if err != nil { 207 | return err 208 | } 209 | log.WithField("index", i).Debug("Picked EC2 Instance") 210 | if i < 1 || i > len(instances) { 211 | return fmt.Errorf("instance index out of range: %d", i) 212 | } 213 | instance := instances[i-1] 214 | err = provider.StartSession(aws.TargetTypeInstanceID, instance.ID) 215 | if err != nil { 216 | return err 217 | } 218 | } 219 | return nil 220 | } 221 | 222 | func (input *StartInput) listSessions(provider aws.CloudSessions) error { 223 | sessions, err := provider.ListSessions() 224 | if err != nil { 225 | log.Error("Failed listing instances") 226 | return err 227 | } 228 | outString, err := sessionsToString(*input.OutputFormat, sessions) 229 | if err != nil { 230 | log.Error("Failed stringifying instances") 231 | return err 232 | } 233 | // TODO Mock stdout 234 | fmt.Fprint(os.Stdout, outString) 235 | if *input.Interactive && len(sessions) > 0 { 236 | // TODO Mock stdin and stderr 237 | reader := bufio.NewReader(os.Stdin) 238 | fmt.Fprintf(os.Stderr, "Terminate session [1 - %d]: ", len(sessions)) 239 | textInput, err := reader.ReadString('\n') 240 | if err != nil { 241 | return err 242 | } 243 | i, err := strconv.Atoi(strings.ReplaceAll(textInput, "\n", "")) 244 | if err != nil { 245 | return err 246 | } 247 | log.WithField("index", i).Debug("Picked session") 248 | if i < 1 || i > len(sessions) { 249 | return fmt.Errorf("session index out of range: %d", i) 250 | } 251 | chosenSession := sessions[i-1] 252 | err = provider.TerminateSession(chosenSession.SessionID) 253 | if err != nil { 254 | return err 255 | } 256 | } 257 | return nil 258 | } 259 | -------------------------------------------------------------------------------- /pkg/list/list_test.go: -------------------------------------------------------------------------------- 1 | package list 2 | 3 | // AWS Mock generation 4 | //go:generate go run github.com/golang/mock/mockgen -self_package=github.com/danmx/sigil/pkg/list -package list -destination aws_mock_test.go github.com/danmx/sigil/pkg/aws Cloud,CloudInstances,CloudSessions,CloudSSH 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/danmx/sigil/pkg/aws" 10 | 11 | "github.com/golang/mock/gomock" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | // TestInstancesString verifies correctness of preparing a list of instanes 16 | func TestInstancesString(t *testing.T) { 17 | resultText := `Index Name Instance ID IP Address Private DNS Name 18 | 1 testNode1 i-xxxxxxxxxxxxxxxx1 10.10.10.1 test1.local 19 | 2 testNode2 i-xxxxxxxxxxxxxxxx2 10.10.10.2 test2.local 20 | ` 21 | resultWide := `Index Name Instance ID IP Address Private DNS Name Hostname OS Name OS Version OS Type 22 | 1 testNode1 i-xxxxxxxxxxxxxxxx1 10.10.10.1 test1.local testHostname1 Amazon Linux 2 Linux 23 | 2 testNode2 i-xxxxxxxxxxxxxxxx2 10.10.10.2 test2.local testHostname2 Ubuntu 18.04 Linux 24 | ` 25 | resultJSON := `[{"hostname":"testHostname1","ip_address":"10.10.10.1","id":"i-xxxxxxxxxxxxxxxx1","private_dns_name":"test1.local","name":"testNode1","os_name":"Amazon Linux","os_type":"Linux","os_version":"2"},{"hostname":"testHostname2","ip_address":"10.10.10.2","id":"i-xxxxxxxxxxxxxxxx2","private_dns_name":"test2.local","name":"testNode2","os_name":"Ubuntu","os_type":"Linux","os_version":"18.04"}] 26 | ` 27 | resultYAML := `- hostname: testHostname1 28 | ip_address: 10.10.10.1 29 | id: i-xxxxxxxxxxxxxxxx1 30 | private_dns_name: test1.local 31 | name: testNode1 32 | os_name: Amazon Linux 33 | os_type: Linux 34 | os_version: "2" 35 | - hostname: testHostname2 36 | ip_address: 10.10.10.2 37 | id: i-xxxxxxxxxxxxxxxx2 38 | private_dns_name: test2.local 39 | name: testNode2 40 | os_name: Ubuntu 41 | os_type: Linux 42 | os_version: "18.04" 43 | ` 44 | 45 | instances := []*aws.Instance{ 46 | { 47 | Hostname: "testHostname1", 48 | IPAddress: "10.10.10.1", 49 | ID: "i-xxxxxxxxxxxxxxxx1", 50 | PrivateDNSName: "test1.local", 51 | Name: "testNode1", 52 | OSName: "Amazon Linux", 53 | OSType: "Linux", 54 | OSVersion: "2", 55 | }, 56 | { 57 | Hostname: "testHostname2", 58 | IPAddress: "10.10.10.2", 59 | ID: "i-xxxxxxxxxxxxxxxx2", 60 | PrivateDNSName: "test2.local", 61 | Name: "testNode2", 62 | OSName: "Ubuntu", 63 | OSType: "Linux", 64 | OSVersion: "18.04", 65 | }, 66 | } 67 | 68 | a := assert.New(t) 69 | 70 | _, err := instancesToString("wrong", instances) 71 | a.NotNil(err) 72 | outString, err := instancesToString(FormatText, instances) 73 | a.Nil(err) 74 | a.Equal(resultText, outString) 75 | outString, err = instancesToString(FormatWide, instances) 76 | a.Nil(err) 77 | a.Equal(resultWide, outString) 78 | outString, err = instancesToString(FormatJSON, instances) 79 | a.Nil(err) 80 | a.Equal(resultJSON, outString) 81 | outString, err = instancesToString(FormatYAML, instances) 82 | a.Nil(err) 83 | a.Equal(resultYAML, outString) 84 | } 85 | 86 | // TestSessionsString verifies correctness of preparing a list of sessions 87 | func TestSessionsString(t *testing.T) { 88 | resultText := `Index Session ID Target Start Date 89 | 1 test-1234567890 i-xxxxxxxxxxxxxxxx1 2019-05-03T10:08:44Z 90 | ` 91 | resultWide := `Index Session ID Target Start Date Owner Status 92 | 1 test-1234567890 i-xxxxxxxxxxxxxxxx1 2019-05-03T10:08:44Z arn:aws:sts::0123456789:assumed-role/test/test Connected 93 | ` 94 | resultJSON := `[{"session_id":"test-1234567890","target":"i-xxxxxxxxxxxxxxxx1","status":"Connected","start_date":"2019-05-03T10:08:44Z","owner":"arn:aws:sts::0123456789:assumed-role/test/test"}] 95 | ` 96 | resultYAML := `- session_id: test-1234567890 97 | target: i-xxxxxxxxxxxxxxxx1 98 | status: Connected 99 | start_date: "2019-05-03T10:08:44Z" 100 | owner: arn:aws:sts::0123456789:assumed-role/test/test 101 | ` 102 | 103 | sessions := []*aws.Session{ 104 | { 105 | SessionID: "test-1234567890", 106 | Target: "i-xxxxxxxxxxxxxxxx1", 107 | Status: "Connected", 108 | StartDate: "2019-05-03T10:08:44Z", 109 | Owner: "arn:aws:sts::0123456789:assumed-role/test/test", 110 | }, 111 | } 112 | 113 | a := assert.New(t) 114 | 115 | _, err := sessionsToString("wrong", sessions) 116 | a.NotNil(err) 117 | outString, err := sessionsToString(FormatText, sessions) 118 | a.Nil(err) 119 | a.Equal(resultText, outString) 120 | outString, err = sessionsToString(FormatWide, sessions) 121 | a.Nil(err) 122 | a.Equal(resultWide, outString) 123 | outString, err = sessionsToString(FormatJSON, sessions) 124 | a.Nil(err) 125 | a.Equal(resultJSON, outString) 126 | outString, err = sessionsToString(FormatYAML, sessions) 127 | a.Nil(err) 128 | a.Equal(resultYAML, outString) 129 | } 130 | 131 | // TestListInstances verifies listing instances 132 | func TestListInstances(t *testing.T) { 133 | instances := []*aws.Instance{ 134 | { 135 | Hostname: "testHostname1", 136 | IPAddress: "10.10.10.1", 137 | ID: "i-xxxxxxxxxxxxxxxx1", 138 | PrivateDNSName: "test1.local", 139 | Name: "testNode1", 140 | OSName: "Amazon Linux", 141 | OSType: "Linux", 142 | OSVersion: "2", 143 | }, 144 | { 145 | Hostname: "testHostname2", 146 | IPAddress: "10.10.10.2", 147 | ID: "i-xxxxxxxxxxxxxxxx2", 148 | PrivateDNSName: "test2.local", 149 | Name: "testNode2", 150 | OSName: "Ubuntu", 151 | OSType: "Linux", 152 | OSVersion: "18.04", 153 | }, 154 | } 155 | interactive := false 156 | format := FormatText 157 | input := StartInput{ 158 | OutputFormat: &format, 159 | Interactive: &interactive, 160 | } 161 | 162 | ctrl := gomock.NewController(t) 163 | defer ctrl.Finish() 164 | m := NewMockCloudInstances(ctrl) // skipcq: SCC-compile 165 | 166 | m.EXPECT().ListInstances().Return(instances, nil) 167 | 168 | assert.NoError(t, input.listInstances(m)) 169 | // TODO test integractive part 170 | } 171 | 172 | // TestListSessions verifies listing sessions 173 | func TestListSessions(t *testing.T) { 174 | sessions := []*aws.Session{ 175 | { 176 | SessionID: "test-1234567890", 177 | Target: "i-xxxxxxxxxxxxxxxx1", 178 | Status: "Connected", 179 | StartDate: "2019-05-03T10:08:44Z", 180 | Owner: "arn:aws:sts::0123456789:assumed-role/test/test", 181 | }, 182 | } 183 | interactive := false 184 | format := FormatText 185 | input := StartInput{ 186 | OutputFormat: &format, 187 | Interactive: &interactive, 188 | } 189 | 190 | ctrl := gomock.NewController(t) 191 | defer ctrl.Finish() 192 | m := NewMockCloudSessions(ctrl) // skipcq: SCC-compile 193 | 194 | m.EXPECT().ListSessions().Return(sessions, nil) 195 | 196 | assert.NoError(t, input.listSessions(m)) 197 | // TODO test integractive part 198 | } 199 | -------------------------------------------------------------------------------- /pkg/os/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = [ 6 | "ignore_signals.go", 7 | "ignore_signals_windows.go", 8 | ], 9 | importpath = "github.com/danmx/sigil/pkg/os", 10 | visibility = ["//visibility:public"], 11 | ) 12 | -------------------------------------------------------------------------------- /pkg/os/ignore_signals.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package os 5 | 6 | import ( 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | ) 11 | 12 | // IgnoreUserEnteredSignals ignores user signals 13 | func IgnoreUserEnteredSignals() { 14 | signals := []os.Signal{syscall.SIGINT, syscall.SIGSTOP, syscall.SIGTSTP, syscall.SIGQUIT} 15 | for _, s := range signals { 16 | signal.Ignore(s) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /pkg/os/ignore_signals_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package os 5 | 6 | import ( 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | ) 11 | 12 | // IgnoreUserEnteredSignals ignores user signals 13 | func IgnoreUserEnteredSignals() { 14 | signals := []os.Signal{syscall.SIGINT} 15 | for _, s := range signals { 16 | signal.Ignore(s) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /pkg/session/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") 2 | load("@com_github_jmhodges_bazel_gomock//:gomock.bzl", "gomock") 3 | 4 | go_library( 5 | name = "go_default_library", 6 | srcs = ["session.go"], 7 | importpath = "github.com/danmx/sigil/pkg/session", 8 | visibility = ["//visibility:public"], 9 | deps = [ 10 | "//pkg/aws:go_default_library", 11 | "@com_github_sirupsen_logrus//:go_default_library", 12 | ], 13 | ) 14 | 15 | go_test( 16 | name = "go_default_test", 17 | srcs = [ 18 | "aws_mock_test.go", 19 | "session_test.go", 20 | ], 21 | embed = [":go_default_library"], 22 | deps = [ 23 | "//pkg/aws:go_default_library", 24 | "@com_github_golang_mock//gomock:go_default_library", 25 | "@com_github_stretchr_testify//assert:go_default_library", 26 | ], 27 | ) 28 | 29 | gomock( 30 | name = "mock_aws", 31 | out = "aws_mock_test.go", 32 | interfaces = [ 33 | "Cloud", 34 | "CloudInstances", 35 | "CloudSessions", 36 | "CloudSSH", 37 | ], 38 | library = "//pkg/aws:go_default_library", 39 | package = "session", 40 | self_package = "github.com/danmx/sigil/pkg/session", 41 | ) 42 | -------------------------------------------------------------------------------- /pkg/session/session.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "github.com/danmx/sigil/pkg/aws" 5 | 6 | log "github.com/sirupsen/logrus" 7 | ) 8 | 9 | // Session wraps methods used from the pkg/session package 10 | type Session interface { 11 | Start(input *StartInput) error 12 | } 13 | 14 | // StartInput struct contains all input data 15 | type StartInput struct { 16 | Target *string 17 | TargetType *string 18 | MFAToken *string 19 | Region *string 20 | Profile *string 21 | Trace *bool 22 | } 23 | 24 | // Start will start a session in chosen instance 25 | func Start(input *StartInput) error { 26 | return input.start(new(aws.Provider)) 27 | } 28 | 29 | func (input *StartInput) start(provider aws.CloudInstances) error { 30 | err := provider.NewWithConfig(&aws.Config{ 31 | Region: *input.Region, 32 | Profile: *input.Profile, 33 | MFAToken: *input.MFAToken, 34 | Trace: *input.Trace, 35 | }) 36 | if err != nil { 37 | log.Error("Failed to generate new provider") 38 | return err 39 | } 40 | if err := provider.StartSession(*input.TargetType, *input.Target); err != nil { 41 | log.WithFields(log.Fields{ 42 | "target": *input.Target, 43 | "targetType": *input.TargetType, 44 | }).Error("Failed to start a session") 45 | return err 46 | } 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /pkg/session/session_test.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | // AWS Mock generation 4 | //go:generate go run github.com/golang/mock/mockgen -self_package=github.com/danmx/sigil/pkg/session -package session -destination aws_mock_test.go github.com/danmx/sigil/pkg/aws Cloud,CloudInstances,CloudSessions,CloudSSH 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/danmx/sigil/pkg/aws" 10 | 11 | "github.com/golang/mock/gomock" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | // TestStart verifies start session method for different configurations 16 | func TestStart(t *testing.T) { 17 | ctrl := gomock.NewController(t) 18 | defer ctrl.Finish() 19 | m := NewMockCloudInstances(ctrl) // skipcq: SCC-compile 20 | 21 | mfa := "123456" 22 | region := "eu-west-1" 23 | profile := "west" 24 | trace := false 25 | input := StartInput{ 26 | MFAToken: &mfa, 27 | Region: ®ion, 28 | Profile: &profile, 29 | Trace: &trace, 30 | } 31 | // Instance ID 32 | target := "i-xxxxxxxxxxxxxxxx1" 33 | targetType := aws.TargetTypeInstanceID 34 | input.Target = &target 35 | input.TargetType = &targetType 36 | gomock.InOrder( 37 | m.EXPECT().NewWithConfig(gomock.Eq(&aws.Config{ 38 | Region: *input.Region, 39 | Profile: *input.Profile, 40 | MFAToken: *input.MFAToken, 41 | Trace: *input.Trace, 42 | })).Return(nil), 43 | m.EXPECT().StartSession(gomock.Eq(*input.TargetType), gomock.Eq(*input.Target)).Return(nil), 44 | ) 45 | assert.NoError(t, input.start(m)) 46 | // DNS 47 | target = "test.local" 48 | targetType = aws.TargetTypePrivateDNS 49 | gomock.InOrder( 50 | m.EXPECT().NewWithConfig(gomock.Eq(&aws.Config{ 51 | Region: *input.Region, 52 | Profile: *input.Profile, 53 | MFAToken: *input.MFAToken, 54 | Trace: *input.Trace, 55 | })).Return(nil), 56 | m.EXPECT().StartSession(gomock.Eq(*input.TargetType), gomock.Eq(*input.Target)).Return(nil), 57 | ) 58 | assert.NoError(t, input.start(m)) 59 | // Name 60 | target = "Backend" 61 | targetType = aws.TargetTypeName 62 | gomock.InOrder( 63 | m.EXPECT().NewWithConfig(gomock.Eq(&aws.Config{ 64 | Region: *input.Region, 65 | Profile: *input.Profile, 66 | MFAToken: *input.MFAToken, 67 | Trace: *input.Trace, 68 | })).Return(nil), 69 | m.EXPECT().StartSession(gomock.Eq(*input.TargetType), gomock.Eq(*input.Target)).Return(nil), 70 | ) 71 | assert.NoError(t, input.start(m)) 72 | } 73 | -------------------------------------------------------------------------------- /pkg/ssh/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") 2 | load("@com_github_jmhodges_bazel_gomock//:gomock.bzl", "gomock") 3 | 4 | go_library( 5 | name = "go_default_library", 6 | srcs = ["ssh.go"], 7 | importpath = "github.com/danmx/sigil/pkg/ssh", 8 | visibility = ["//visibility:public"], 9 | deps = [ 10 | "//pkg/aws:go_default_library", 11 | "@com_github_sirupsen_logrus//:go_default_library", 12 | "@org_golang_x_crypto//ssh:go_default_library", 13 | ], 14 | ) 15 | 16 | go_test( 17 | name = "go_default_test", 18 | srcs = [ 19 | "aws_mock_test.go", 20 | "ssh_test.go", 21 | ], 22 | embed = [":go_default_library"], 23 | deps = [ 24 | "//pkg/aws:go_default_library", 25 | "@com_github_golang_mock//gomock:go_default_library", 26 | "@com_github_stretchr_testify//assert:go_default_library", 27 | ], 28 | ) 29 | 30 | gomock( 31 | name = "mock_aws", 32 | out = "aws_mock_test.go", 33 | interfaces = [ 34 | "Cloud", 35 | "CloudInstances", 36 | "CloudSessions", 37 | "CloudSSH", 38 | ], 39 | library = "//pkg/aws:go_default_library", 40 | package = "ssh", 41 | self_package = "github.com/danmx/sigil/pkg/ssh", 42 | ) 43 | -------------------------------------------------------------------------------- /pkg/ssh/ssh.go: -------------------------------------------------------------------------------- 1 | package ssh 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/rsa" 6 | "crypto/x509" 7 | "encoding/pem" 8 | "io/ioutil" 9 | "os" 10 | "strings" 11 | 12 | "github.com/danmx/sigil/pkg/aws" 13 | 14 | log "github.com/sirupsen/logrus" 15 | "golang.org/x/crypto/ssh" 16 | ) 17 | 18 | // SSH wraps methods used from the pkg/ssh package 19 | type SSH interface { 20 | Start(input *StartInput) error 21 | } 22 | 23 | // StartInput struct contains all input data 24 | type StartInput struct { 25 | Target *string 26 | TargetType *string 27 | PortNumber *uint64 28 | PublicKey *string 29 | OSUser *string 30 | GenKeyPair *bool 31 | MFAToken *string 32 | Region *string 33 | Profile *string 34 | Trace *bool 35 | } 36 | 37 | // Start will start ssh session 38 | func Start(input *StartInput) error { 39 | return input.start(new(aws.Provider)) 40 | } 41 | 42 | func (input *StartInput) start(provider aws.CloudSSH) error { 43 | err := provider.NewWithConfig(&aws.Config{ 44 | Region: *input.Region, 45 | Profile: *input.Profile, 46 | MFAToken: *input.MFAToken, 47 | Trace: *input.Trace, 48 | }) 49 | if err != nil { 50 | log.Error(err) 51 | return err 52 | } 53 | pubKey := *input.PublicKey 54 | if *input.GenKeyPair { 55 | const rsaKeySize = 4092 56 | privKeyBlob, errKey := rsa.GenerateKey(rand.Reader, rsaKeySize) 57 | if errKey != nil { 58 | return errKey 59 | } 60 | pubKeyBlob := privKeyBlob.PublicKey 61 | if errPubPEM := savePublicPEMKey(pubKey, &pubKeyBlob); errPubPEM != nil { 62 | return errPubPEM 63 | } 64 | defer func() { 65 | if err = deleteTempKey(pubKey); err != nil { 66 | log.Error(err) 67 | } 68 | }() 69 | privKey := strings.TrimSuffix(pubKey, ".pub") 70 | if errPrivPEM := savePrivPEMKey(privKey, privKeyBlob); errPrivPEM != nil { 71 | return errPrivPEM 72 | } 73 | defer func() { 74 | if err = deleteTempKey(privKey); err != nil { 75 | log.Error(err) 76 | } 77 | }() 78 | } 79 | 80 | pubKeyData := []byte{} 81 | if pubKey != "" { 82 | pubKeyData, err = ioutil.ReadFile(pubKey) 83 | if err != nil { 84 | return err 85 | } 86 | } 87 | 88 | log.WithFields(log.Fields{ 89 | "targetType": *input.TargetType, 90 | "PortNumber": *input.PortNumber, 91 | "target": *input.Target, 92 | "OSUser": *input.OSUser, 93 | "pubKeyData": string(pubKeyData), 94 | "PublicKeyPath": pubKey, 95 | }).Debug("StartSSHInput") 96 | 97 | // returns err 98 | return provider.StartSSH(*input.TargetType, *input.Target, *input.OSUser, *input.PortNumber, pubKeyData) 99 | } 100 | 101 | // Helper functions 102 | 103 | func savePrivPEMKey(fileName string, key *rsa.PrivateKey) error { 104 | var privateKey = &pem.Block{ 105 | Type: "RSA PRIVATE KEY", 106 | Headers: nil, 107 | Bytes: x509.MarshalPKCS1PrivateKey(key), 108 | } 109 | 110 | // returns err 111 | return ioutil.WriteFile(fileName, pem.EncodeToMemory(privateKey), 0600) //nolint:gomnd // Linux file permissions 112 | } 113 | 114 | func savePublicPEMKey(fileName string, pubkey *rsa.PublicKey) error { 115 | pub, err := ssh.NewPublicKey(pubkey) 116 | if err != nil { 117 | return err 118 | } 119 | // returns err 120 | return ioutil.WriteFile(fileName, ssh.MarshalAuthorizedKey(pub), 0600) //nolint:gomnd // Linux file permissions 121 | } 122 | 123 | func deleteTempKey(keyPath string) error { 124 | stat, err := os.Stat(keyPath) 125 | log.WithFields(log.Fields{ 126 | "stat": stat, 127 | "err": err, 128 | }).Debug("Checking if key exist") 129 | if err == nil { 130 | if errRm := os.Remove(keyPath); errRm != nil { 131 | return errRm 132 | } 133 | } 134 | return err 135 | } 136 | -------------------------------------------------------------------------------- /pkg/ssh/ssh_test.go: -------------------------------------------------------------------------------- 1 | package ssh 2 | 3 | // AWS Mock generation 4 | //go:generate go run github.com/golang/mock/mockgen -self_package=github.com/danmx/sigil/pkg/ssh -package ssh -destination aws_mock_test.go github.com/danmx/sigil/pkg/aws Cloud,CloudInstances,CloudSessions,CloudSSH 5 | 6 | import ( 7 | "os" 8 | "path" 9 | "testing" 10 | 11 | "github.com/danmx/sigil/pkg/aws" 12 | 13 | "github.com/golang/mock/gomock" 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | // TestStart verifies start ssh method 18 | func TestStart(t *testing.T) { 19 | // Instance 20 | ctrl := gomock.NewController(t) 21 | defer ctrl.Finish() 22 | m := NewMockCloudSSH(ctrl) // skipcq: SCC-compile 23 | 24 | target := "i-xxxxxxxxxxxxxxxx1" 25 | targetType := aws.TargetTypeInstanceID 26 | mfa := "123456" 27 | region := "eu-west-1" 28 | profile := "west" 29 | var port uint64 = 22 30 | pubKey := path.Join(os.TempDir(), "sigil_test.pub") 31 | osUser := "ec2-user" 32 | genKey := true 33 | trace := false 34 | input := StartInput{ 35 | MFAToken: &mfa, 36 | Region: ®ion, 37 | Profile: &profile, 38 | Target: &target, 39 | TargetType: &targetType, 40 | PortNumber: &port, 41 | PublicKey: &pubKey, 42 | OSUser: &osUser, 43 | GenKeyPair: &genKey, 44 | Trace: &trace, 45 | } 46 | 47 | gomock.InOrder( 48 | m.EXPECT().NewWithConfig(gomock.Eq(&aws.Config{ 49 | Region: *input.Region, 50 | Profile: *input.Profile, 51 | MFAToken: *input.MFAToken, 52 | Trace: *input.Trace, 53 | })).Return(nil), 54 | m.EXPECT().StartSSH( 55 | gomock.Eq(*input.TargetType), 56 | gomock.Eq(*input.Target), 57 | gomock.Eq(*input.OSUser), 58 | gomock.Eq(*input.PortNumber), 59 | gomock.Any(), 60 | ).Return(nil), 61 | ) 62 | 63 | assert.NoError(t, input.start(m)) 64 | 65 | // DNS 66 | target = "test.local" 67 | targetType = aws.TargetTypePrivateDNS 68 | input = StartInput{ 69 | MFAToken: &mfa, 70 | Region: ®ion, 71 | Profile: &profile, 72 | Target: &target, 73 | TargetType: &targetType, 74 | PortNumber: &port, 75 | PublicKey: &pubKey, 76 | OSUser: &osUser, 77 | GenKeyPair: &genKey, 78 | Trace: &trace, 79 | } 80 | 81 | gomock.InOrder( 82 | m.EXPECT().NewWithConfig(gomock.Eq(&aws.Config{ 83 | Region: *input.Region, 84 | Profile: *input.Profile, 85 | MFAToken: *input.MFAToken, 86 | Trace: *input.Trace, 87 | })).Return(nil), 88 | m.EXPECT().StartSSH( 89 | gomock.Eq(*input.TargetType), 90 | gomock.Eq(*input.Target), 91 | gomock.Eq(*input.OSUser), 92 | gomock.Eq(*input.PortNumber), 93 | gomock.Any(), 94 | ).Return(nil), 95 | ) 96 | 97 | assert.NoError(t, input.start(m)) 98 | 99 | // Name 100 | target = "Backend" 101 | targetType = aws.TargetTypePrivateDNS 102 | input = StartInput{ 103 | MFAToken: &mfa, 104 | Region: ®ion, 105 | Profile: &profile, 106 | Target: &target, 107 | TargetType: &targetType, 108 | PortNumber: &port, 109 | PublicKey: &pubKey, 110 | OSUser: &osUser, 111 | GenKeyPair: &genKey, 112 | Trace: &trace, 113 | } 114 | 115 | gomock.InOrder( 116 | m.EXPECT().NewWithConfig(gomock.Eq(&aws.Config{ 117 | Region: *input.Region, 118 | Profile: *input.Profile, 119 | MFAToken: *input.MFAToken, 120 | Trace: *input.Trace, 121 | })).Return(nil), 122 | m.EXPECT().StartSSH( 123 | gomock.Eq(*input.TargetType), 124 | gomock.Eq(*input.Target), 125 | gomock.Eq(*input.OSUser), 126 | gomock.Eq(*input.PortNumber), 127 | gomock.Any(), 128 | ).Return(nil), 129 | ) 130 | 131 | assert.NoError(t, input.start(m)) 132 | 133 | gomock.InOrder( 134 | m.EXPECT().NewWithConfig(gomock.Eq(&aws.Config{ 135 | Region: *input.Region, 136 | Profile: *input.Profile, 137 | MFAToken: *input.MFAToken, 138 | Trace: *input.Trace, 139 | })).Return(nil), 140 | m.EXPECT().StartSSH( 141 | gomock.Eq(*input.TargetType), 142 | gomock.Eq(*input.Target), 143 | gomock.Eq(*input.OSUser), 144 | gomock.Eq(*input.PortNumber), 145 | gomock.Any(), 146 | ).Return(nil), 147 | ) 148 | 149 | assert.NoError(t, input.start(m)) 150 | } 151 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "schedule": "at 7am on Monday" 6 | } 7 | -------------------------------------------------------------------------------- /scripts/pre-commit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | test(){ 6 | bazel test //... 7 | } 8 | 9 | fmt() { 10 | bazel run :gazelle && bazel run @go_sdk//:bin/gofmt -- -s -w . 11 | } 12 | 13 | lint() { 14 | bazel run @com_github_danmx_bazel_tools//golangci-lint:run -- run ./... 15 | } 16 | 17 | update_deps() { 18 | bazel run @go_sdk//:bin/go -- mod tidy && bazelisk run :gazelle -- update-repos -from_file=go.mod -to_macro=tools/repositories.bzl%go_repositories -prune=true 19 | } 20 | 21 | changelog(){ 22 | bazel run @com_github_danmx_bazel_tools//git-chglog:run -- -o CHANGELOG.md 23 | } 24 | 25 | #shellcheck disable=SC2068 26 | $@ 27 | -------------------------------------------------------------------------------- /tools/BUILD: -------------------------------------------------------------------------------- 1 | package(default_visibility = ["//visibility:public"]) 2 | 3 | exports_files(["fix_codecov.sh"]) 4 | -------------------------------------------------------------------------------- /tools/fix_codecov.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | rootDir="bazel-out" 6 | 7 | echo "mode: set" 8 | 9 | while IFS= read -r -d '' i 10 | do 11 | # >&2 echo "${i} -> ${i//.dat/.txt}" 12 | >&2 echo "parsing: ${i}" 13 | # cp "${i}" "${i//.dat/.txt}" 14 | tail -n +2 "${i}" 15 | done < <(find "${rootDir}"/ -name coverage.dat -print0) 16 | -------------------------------------------------------------------------------- /tools/get_workspace_status.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | set -eu 4 | 5 | appName="sigil" 6 | appVersion="${VERSION:-0.7.0}" 7 | gitCommit="${GIT_COMMIT:-$(git rev-parse HEAD)}" 8 | gitBranch="${GIT_BRANCH:-$(git rev-parse --abbrev-ref HEAD)}" 9 | 10 | IFS='.' read -r major minor patch << EOF 11 | ${appVersion} 12 | EOF 13 | 14 | cat <