├── .dockerignore ├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── ruby.yml │ ├── rubygem.yml │ └── rubygem_release.yml ├── .gitignore ├── .tool-versions ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── Gemfile ├── LICENSE.md ├── README.md ├── Rakefile ├── Vagrantfile ├── aws_security_viz.gemspec ├── config └── boot.rb ├── exe └── aws_security_viz ├── images └── sample.png ├── lib ├── aws_config.rb ├── aws_security_viz.rb ├── color_picker.rb ├── debug │ └── parse_log.rb ├── debug_graph.rb ├── ec2 │ ├── ip_permission.rb │ ├── security_groups.rb │ └── traffic.rb ├── exclusions.rb ├── export │ └── html │ │ ├── navigator.html │ │ └── view.html ├── graph.rb ├── graph_filter.rb ├── opts.yml.sample ├── provider │ ├── ec2.rb │ └── json.rb ├── renderer │ ├── all.rb │ ├── graphviz.rb │ ├── json.rb │ └── navigator.rb └── version.rb └── spec ├── color_picker_spec.rb ├── graph_filter_spec.rb ├── integration ├── aws_expected.json ├── dummy.dot ├── dummy.json ├── expected.json ├── navigator.json └── visualize_aws_spec.rb ├── spec_helper.rb └── visualize_aws_spec.rb /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .bundle 3 | *.svg 4 | *.dot 5 | *.png 6 | .DS_Store 7 | navigator.html 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.rb] 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 2 11 | 12 | [Makefile] 13 | indent_style = tab 14 | 15 | [.travis.yml] 16 | indent_style = space 17 | indent_size = 2 -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "bundler" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" 16 | -------------------------------------------------------------------------------- /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | name: Ruby 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 0 * * SAT' 8 | 9 | jobs: 10 | test: 11 | runs-on: ${{matrix.os}}-latest 12 | strategy: 13 | matrix: 14 | os: 15 | - ubuntu 16 | - macos 17 | 18 | ruby: 19 | - 3.1 20 | - 3.2 21 | - 3.3 22 | - 3.4 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | - name: Set up Ruby 27 | uses: ruby/setup-ruby@v1 28 | with: 29 | ruby-version: ${{matrix.ruby}} 30 | - name: Install dependencies 31 | run: bundle install 32 | - name: Run tests 33 | run: bundle exec rspec spec --tag '~integration' 34 | 35 | -------------------------------------------------------------------------------- /.github/workflows/rubygem.yml: -------------------------------------------------------------------------------- 1 | name: Ruby Gem 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | build: 9 | name: Build + Publish 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Set up Ruby 3 15 | uses: ruby/setup-ruby@v1 16 | with: 17 | ruby-version: 3.4 18 | 19 | - name: Publish to RubyGems 20 | run: | 21 | mkdir -p $HOME/.gem 22 | touch $HOME/.gem/credentials 23 | chmod 0600 $HOME/.gem/credentials 24 | printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials 25 | gem build *.gemspec 26 | gem push *.gem 27 | env: 28 | GEM_HOST_API_KEY: "${{secrets.RUBYGEMS_AUTH_TOKEN}}" 29 | ALPHA_BUILD_NUMBER: "${{github.run_number}}" 30 | -------------------------------------------------------------------------------- /.github/workflows/rubygem_release.yml: -------------------------------------------------------------------------------- 1 | name: Ruby Gem Release 2 | 3 | on: 4 | release: 5 | 6 | jobs: 7 | build: 8 | name: Build + Publish 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Set up Ruby 3 14 | uses: ruby/setup-ruby@v1 15 | with: 16 | ruby-version: 3.2 17 | 18 | - name: Publish to RubyGems 19 | run: | 20 | mkdir -p $HOME/.gem 21 | touch $HOME/.gem/credentials 22 | chmod 0600 $HOME/.gem/credentials 23 | printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials 24 | gem build *.gemspec 25 | gem push *.gem 26 | env: 27 | GEM_HOST_API_KEY: "${{secrets.RUBYGEMS_AUTH_TOKEN}}" 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /test/tmp/ 9 | /test/version_tmp/ 10 | /tmp/ 11 | /data/ 12 | 13 | ## Documentation cache and generated files: 14 | /.yardoc/ 15 | /_yardoc/ 16 | /doc/ 17 | /rdoc/ 18 | 19 | ## Environment normalisation: 20 | /.bundle/ 21 | /lib/bundler/man/ 22 | 23 | .ruby-version 24 | .rbenv-gemsets 25 | .gems 26 | .envrc 27 | 28 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 29 | .rvmrc 30 | 31 | opts.yml 32 | 33 | .vagrant 34 | Gemfile.lock 35 | 36 | # in case you run the sample commands in this directory.. 37 | viz.svg 38 | aws.json 39 | navigator.html 40 | *.png 41 | *.dot 42 | *.svg 43 | .DS_Store 44 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | ruby 3.4 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | This project adheres to [Semantic Versioning](http://semver.org/). 4 | 5 | ## [0.2.4] - 2023-06-10 6 | - Matrix builds for Ruby v3.0 onwards only 7 | - Remove support for Ruby v2.x 8 | 9 | ## [0.2.3] - 2021-07-03 10 | ### Added 11 | - Support for Ruby v3 12 | - Matrix builds for Ruby v2.5 to v3.0 13 | 14 | ## [0.2.2] - 2021-01-05 15 | ### Added 16 | - --version option which prints aws-security-viz version 17 | - Switchover to github actions from travis 18 | 19 | ## [0.2.1] - 2020-06-20 20 | ### Added 21 | - Provide a webserver using a --serve PORT which provides a link to the navigator view 22 | 23 | ### Changed 24 | - Bumped InteractiveGraph to v0.3.2 25 | 26 | ### Fixed 27 | - Empty InfoPanel in navigator view now shows vpc, security group name. 28 | 29 | ## [0.2.0] - 2019-01-24 30 | ### Added 31 | - Add graph navigator renderer using https://grapheco.github.io/InteractiveGraph/ 32 | 33 | ### Changed 34 | - Renderer no longer identified automatically based on json file extension. 35 | 36 | ## [0.1.6] - 2019-01-14 37 | ### Added 38 | - Dockerfile 39 | 40 | ### Changed 41 | - Replaced fog gem with aws-sdk-ec2 42 | - Upgrade bundler to 2.x 43 | - Removed unused dependencies 44 | 45 | ### Fixed 46 | - Issue with --color=true failing with exception due to change in Graphviz library. 47 | 48 | ## [0.1.5] - 2018-10-10 49 | ### Added 50 | - Filter by VPC id 51 | - Support for AWS session token 52 | - Use rankdir with graphviz to improve layout 53 | 54 | ### Changed 55 | - Dependent trollop gem renamed to optimist 56 | - Switched from ruby-graphviz to graphviz gem 57 | 58 | ## [0.1.4] - 2017-02-03 59 | ### Added 60 | - CHANGELOG.md 61 | - Support for AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY 62 | - Capability to view filtered view by source or target. 63 | 64 | ### Fixed 65 | - Issue with vagrant up not working due to older bundler version and missing dependencies 66 | 67 | ## [0.1.3] - 2016-03-20 68 | ### Changed 69 | - Removed ENV usage from the gem. 70 | 71 | ### Fixed 72 | - Pointing to specific version of linkurious.js instead of master build 73 | - Web view html and json files are now generated in the same directory. 74 | 75 | ### Added 76 | - Support for credentials via ENV or .fog 77 | - Capability to exclude egress via opts.yml file. 78 | 79 | 80 | ## [0.1.2] - 2015-12-19 81 | ### Added 82 | - linkurious/sigma.js to render json representation of security groups 83 | - Capability to exclude CIDR via opts.yml file 84 | 85 | 86 | ## [0.1.1] - 2015-10-18 87 | ### Added 88 | - Capability to generate an opts.yml file to tweak aws-security-viz behavior. 89 | 90 | ### Removed 91 | - Support for Ruby 1.9.3 92 | 93 | ## 0.1.0 - 2015-10-15 94 | ### Added 95 | - Begin life as the gem [aws_security_viz](https://rubygems.org/gems/aws_security_viz) 96 | 97 | 98 | [Unreleased]: https://github.com/anaynayak/aws-security-viz/compare/v0.1.3...HEAD 99 | [0.1.3]: https://github.com/anaynayak/aws-security-viz/compare/v0.1.2...v0.1.3 100 | [0.1.2]: https://github.com/anaynayak/aws-security-viz/compare/v0.1.1...v0.1.2 101 | [0.1.1]: https://github.com/anaynayak/aws-security-viz/compare/v0.1.0...v0.1.1 102 | 103 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at whynospam-awsviz@yahoo.co.in. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:3.4-alpine 2 | RUN apk add --update \ 3 | build-base \ 4 | graphviz \ 5 | ttf-freefont 6 | RUN gem install aws_security_viz --pre 7 | RUN apk del build-base 8 | WORKDIR /aws-security-viz 9 | CMD ["aws_security_viz"] 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Anay Nayak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | aws-security-viz -- A tool to visualize aws security groups 2 | ============================================================ 3 | [![Build Status](https://github.com/anaynayak/aws-security-viz/workflows/Ruby/badge.svg)](https://github.com/anaynayak/aws-security-viz/actions?query=workflow%3ARuby) 4 | [![License](https://img.shields.io/github/license/anaynayak/aws-security-viz.svg?maxAge=2592000)]() 5 | [![Docker Pulls](https://img.shields.io/docker/pulls/anay/aws-security-viz)](https://hub.docker.com/r/anay/aws-security-viz/) 6 | [![Dependency Status](https://img.shields.io/librariesio/github/anaynayak/aws-security-viz.png?maxAge=259200)](https://libraries.io/github/anaynayak/aws-security-viz) 7 | ![Gem Downloads (for latest version)](https://img.shields.io/gem/dtv/aws_security_viz) 8 | 9 | 10 | ## DESCRIPTION 11 | Need a quick way to visualize your current aws/amazon ec2 security group configuration? aws-security-viz does just that based on the EC2 security group ingress configuration. 12 | 13 | ## FEATURES 14 | 15 | * Output to any of the formats that Graphviz supports. 16 | * EC2 classic and VPC security groups 17 | 18 | ## INSTALLATION 19 | ``` 20 | $ gem install aws_security_viz 21 | $ aws_security_viz --help 22 | ``` 23 | 24 | ## DEPENDENCIES 25 | 26 | * graphviz `brew install graphviz` 27 | 28 | ## USAGE (See Examples section below for more) 29 | 30 | To generate the graph directly using AWS keys 31 | 32 | ``` 33 | $ aws_security_viz -a your_aws_key -s your_aws_secret_key -f viz.svg --color=true 34 | ``` 35 | 36 | To generate the graph using an existing security_groups.json (created using aws-cli) 37 | 38 | ``` 39 | $ aws_security_viz -o data/security_groups.json -f viz.svg --color 40 | ``` 41 | 42 | To generate a web view 43 | 44 | ``` 45 | $ aws_security_viz -a your_aws_key -s your_aws_secret_key -f aws.json --renderer navigator 46 | ``` 47 | 48 | * Generates two files: aws.json and navigator.html. 49 | * The json file name needs to be passed in as a html fragment identifier. 50 | * The generated graph can be viewed in a webserver e.g. http://localhost:3000/navigator.html#aws.json by using `ruby -run -e httpd -- -p 3000` 51 | 52 | ## DOCKER USAGE 53 | 54 | If you don't want to install the dependencies and ruby libs you can execute aws-security-viz inside a docker container. To do so, follow these steps: 55 | 56 | 1. Clone this repository, open it in a console. 57 | 2. Build the docker container: `docker build -t sec-viz .` 58 | 59 | 3.a With aws-vault (Recommended): 60 | 61 | ```aws-vault exec -- docker run -i -e AWS_REGION -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AWS_SESSION_TOKEN -e AWS_SECURITY_TOKEN --rm -t -p 3000:3000 -v (pwd)/aws-viz:/aws-security-viz --name sec-viz sec-viz /usr/local/bundle/bin/aws_security_viz --renderer navigator --serve 3000``` . 62 | 63 | You can open it with your local browser at `http://localhost:3000/navigator.html#aws-security-viz.png`. 64 | 65 | 3.b With AWS credentials passed as parameters: 66 | 67 | ```docker run -i --rm -t -p 3000:3000 -v (pwd)/aws-viz:/aws-security-viz --name sec-viz sec-viz /usr/local/bundle/bin/aws_security_viz -a REPLACE_AWS_ACCESS_KEY_ID -s REPLACE_SECRET --renderer navigator --serve 3000```. 68 | 69 | You can open it with your local browser at `http://localhost:3000/navigator.html#aws-security-viz.png`. 70 | 71 | Parameters passed to the docker command: 72 | * `-v $(pwd)/aws-viz:aws-security-viz` local directory where output will be generated. 73 | * `-i` interactive shell 74 | * `--rm` remove the container after usage 75 | * `-t` attach this terminal to it 76 | * `-p 3000:3000` we expose port 3000 for the HTTP server 77 | * `-name sec-viz` the container will have the same name as the image we will start 78 | 79 | You can also use other parameters as specified in [usage](#USAGE) 80 | 81 | ### Help 82 | 83 | ``` 84 | $ aws_security_viz --help 85 | Options: 86 | -a, --access-key= AWS access key 87 | -s, --secret-key= AWS secret key 88 | -e, --session-token= AWS session token 89 | -r, --region= AWS region to query (default: us-east-1) 90 | -v, --vpc-id= AWS VPC id to show 91 | -o, --source-file= JSON source file containing security groups 92 | -f, --filename= Output file name (default: aws-security-viz.png) 93 | -c, --config= Config file (opts.yml) (default: opts.yml) 94 | -l, --color Colored node edges 95 | -u, --source-filter= Source filter 96 | -t, --target-filter= Target filter 97 | --serve= Serve a HTTP server at specified port 98 | -h, --help Show this message 99 | ``` 100 | 101 | #### Configuration 102 | 103 | aws-security-viz only uses the `ec2:DescribeSecurityGroups` api so a minimal IAM policy which grants only `ec2:DescribeSecurityGroups` access should be enough. 104 | 105 | ```json 106 | { 107 | "Version": "2012-10-17", 108 | "Statement": [ 109 | { 110 | "Effect": "Allow", 111 | "Action": "ec2:DescribeSecurityGroups", 112 | "Resource": "*" 113 | } 114 | ] 115 | } 116 | ``` 117 | 118 | Alternatively you can use [aws-vault](https://github.com/99designs/aws-vault/) and run it using short lived temporary credentials. 119 | 120 | `$ aws-vault exec -- aws_security_viz -f aws.json --renderer navigator --serve 9091` 121 | 122 | #### Advanced configuration 123 | 124 | You can generate a configuration file using the following command: 125 | ``` 126 | $ aws_security_viz setup [-c opts.yml] 127 | ``` 128 | 129 | The opts.yml file lets you define the following options: 130 | 131 | * Grouping of CIDR ips 132 | * Define exclusion patterns 133 | * Change graphviz format (neato, dot, sfdp etc) 134 | 135 | ## DEBUGGING 136 | 137 | To generate the graph with debug statements, execute the following command 138 | 139 | ``` 140 | $ DEBUG=true aws_security_viz -a your_aws_key -s your_aws_secret_key -f viz.svg 141 | ``` 142 | 143 | If it doesn't indicate the problem, please share the generated json file with me @ whynospam-awsviz@yahoo.co.in 144 | 145 | You can send me an obfuscated version using the following command: 146 | 147 | ``` 148 | $ DEBUG=true OBFUSCATE=true aws_security_viz -a your_aws_key -s your_aws_secret_key -f viz.svg 149 | ``` 150 | 151 | Execute the following command to generate the json. You will need [aws-cli](https://github.com/aws/aws-cli) to execute the command 152 | 153 | `aws ec2 describe-security-groups` 154 | 155 | 156 | ## EXAMPLES 157 | 158 | #### Graphviz export 159 | 160 | ![](https://github.com/anaynayak/aws-security-viz/raw/main/images/sample.png) 161 | 162 | #### Navigator view (useful with very large number of nodes) 163 | Via navigator renderer `aws_security_viz -a your_aws_key -s your_aws_secret_key -f aws.json --renderer navigator` 164 | ![](https://user-images.githubusercontent.com/416211/51426583-bb5e0180-1c12-11e9-903b-7b2a2d354ede.png) 165 | 166 | #### JSON view 167 | Via json renderer `aws_security_viz -a your_aws_key -s your_aws_secret_key -f aws.json --renderer json` 168 | ![](https://cloud.githubusercontent.com/assets/416211/11912582/0e66cdbc-a669-11e5-82ab-1e26e3c6949b.png) 169 | 170 | ## Additional examples 171 | 172 | #### Generate `aws-security-viz.png` image for `us-west-1` region 173 | 174 | ``` 175 | $ aws_security_viz --region us-west-1 -f aws-security-viz.png 176 | ``` 177 | 178 | #### Generate visualization for `us-west-1` with target filter as `sec-group-1`. This will display all routes through which we can arrive at `sec-group-1` 179 | 180 | ``` 181 | $ aws_security_viz --region us-west-1 --target-filter=sec-group-1 182 | ``` 183 | 184 | #### Generate visualization for `us-west-1` restricted to vpc-id `vpc-12345` 185 | ``` 186 | $ aws_security_viz --region us-west-1 --vpc-id=vpc-12345 187 | ``` 188 | 189 | #### Generate visualization for `us-west-1` restricted to vpc-id `vpc-12345` 190 | ``` 191 | $ aws_security_viz --region us-west-1 --vpc-id=vpc-12345 192 | ``` 193 | 194 | #### Serve webserver for the navigator view at port 3000 195 | ``` 196 | $ aws_security_viz -a your_aws_key -s your_aws_secret_key -f aws.json --renderer navigator --serve 3000 197 | ``` 198 | The browser link to the view is printed on the CLI 199 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | namespace :docker do 7 | task :login do 8 | sh 'echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin' 9 | end 10 | desc "push to dockerhub" 11 | task :push => :login do 12 | sh 'docker build -t anay/aws-security-viz .' 13 | sh 'docker push anay/aws-security-viz' 14 | end 15 | end -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure(2) do |config| 5 | config.vm.box = "ubuntu/vivid64" 6 | config.vm.provision "shell", inline: <<-SHELL 7 | apt-get -y update 8 | apt-get -y install git ruby-dev ruby graphviz libxml2-dev g++ zlib1g-dev 9 | SHELL 10 | 11 | config.vm.provision "shell", privileged: false, inline: <<-SHELL 12 | git clone https://github.com/anaynayak/aws-security-viz.git 13 | cd aws-security-viz 14 | sudo gem install bundler 15 | bundle install 16 | # bundle exec ruby lib/visualize_aws.rb -a your_aws_key -s your_aws_secret_key -f viz.svg --color=true 17 | SHELL 18 | end 19 | -------------------------------------------------------------------------------- /aws_security_viz.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('../lib', __FILE__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require 'version' 4 | 5 | Gem::Specification.new do |s| 6 | s.name = 'aws_security_viz' 7 | s.version = AwsSecurityViz::VERSION 8 | s.version = "#{s.version}-alpha-#{ENV['ALPHA_BUILD_NUMBER']}" if ENV['ALPHA_BUILD_NUMBER'] 9 | s.date = Time.now.strftime('%Y-%m-%d') 10 | s.summary = 'Visualize your aws security groups' 11 | s.description = 'Provides a quick mechanism to visualize your EC2 security groups in multiple formats' 12 | s.authors = ['Anay Nayak'] 13 | s.email = 'anayak007+rubygems@gmail.com' 14 | s.files = %w(lib config) 15 | s.homepage = 'https://github.com/anaynayak/aws-security-viz' 16 | s.license = 'MIT' 17 | s.bindir = 'exe' 18 | 19 | s.files = `git ls-files -z`.split("\x0") 20 | s.executables = s.files.grep(%r{^exe/}) { |f| File.basename(f) } 21 | s.test_files = s.files.grep(%r{^(test|spec|features)/}) 22 | s.require_paths = ['lib'] 23 | 24 | s.add_development_dependency 'rake', '>= 12.0.0', '~> 13.0' 25 | s.add_development_dependency 'rspec', '~> 3.5', '>= 3.5.0' 26 | if ENV["COVERAGE"] 27 | s.add_development_dependency "simplecov" 28 | end 29 | s.add_runtime_dependency 'rexml', '~> 3.2', '>= 3.2.2' 30 | s.add_runtime_dependency 'graphviz', '~> 1.1', '>= 1.1.0' 31 | s.add_runtime_dependency 'optimist', '>= 3.0', '< 3.3' 32 | s.add_runtime_dependency 'organic_hash', '~> 1.0', '>= 1.0.2' 33 | s.add_runtime_dependency 'rgl', '>= 0.5.3', '< 0.7.0' 34 | s.add_runtime_dependency 'webrick', '>= 1.8.1', '< 1.10.0' 35 | s.add_runtime_dependency 'aws-sdk-ec2', '>= 1.65', '< 1.499' 36 | s.add_runtime_dependency 'base64', '~> 0.2.0' 37 | 38 | s.required_ruby_version = '>= 3.0.0' 39 | end 40 | 41 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler.setup 3 | 4 | Dir["./lib/**/*.rb"].each { |f| require f } 5 | -------------------------------------------------------------------------------- /exe/aws_security_viz: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'aws_security_viz' 4 | require 'optimist' 5 | require 'webrick' 6 | require 'version' 7 | 8 | opts = Optimist::options do 9 | version "aws_security_viz v#{AwsSecurityViz::VERSION}" 10 | opt :access_key, 'AWS access key', :default => ENV['AWS_ACCESS_KEY'] || ENV['AWS_ACCESS_KEY_ID'], :type => :string 11 | opt :secret_key, 'AWS secret key', :default => ENV['AWS_SECRET_KEY'] || ENV['AWS_SECRET_ACCESS_KEY'], :type => :string 12 | opt :session_token, 'AWS session token', :default => ENV['AWS_SESSION_TOKEN'] || nil, :type => :string 13 | opt :region, 'AWS region to query', :default => 'us-east-1', :type => :string 14 | opt :vpc_id, 'AWS VPC id to show', :type => :string 15 | opt :source_file, 'JSON source file containing security groups', :type => :string 16 | opt :filename, 'Output file name', :type => :string, :default => 'aws-security-viz.png' 17 | opt :config, 'Config file (opts.yml)', :type => :string, :default => 'opts.yml' 18 | opt :color, 'Colored node edges', :default => false 19 | opt :renderer, "Renderer (#{Renderer.all.join('|')})", :default => 'graphviz' 20 | opt :source_filter, 'Source filter', :default => nil, :type => :string 21 | opt :target_filter, 'Target filter', :default => nil, :type => :string 22 | opt :serve, 'Serve a HTTP server', :default => nil, :type => :integer 23 | end 24 | 25 | cmd = ARGV.shift 26 | if cmd=="setup" 27 | AwsConfig.write(opts[:config]) 28 | puts "#{opts[:config]} created in current directory." 29 | exit 30 | end 31 | 32 | config = AwsConfig.load(opts[:config]).merge(obfuscate: ENV['OBFUSCATE'], debug: ENV['DEBUG']) 33 | begin 34 | VisualizeAws.new(config, opts).unleash(opts[:filename]) 35 | if opts[:serve] 36 | puts "Navigate to http://localhost:#{opts[:serve]}/navigator.html##{opts[:filename]}" 37 | WEBrick::HTTPServer.new({Port: opts[:serve], DocumentRoot: '.'}).start 38 | end 39 | rescue Exception => e 40 | puts "[ERROR] #{e.message}" 41 | raise e if config.debug? 42 | exit 1 43 | end 44 | -------------------------------------------------------------------------------- /images/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anaynayak/aws-security-viz/5191ef658c1321aefa8f0994a8df4b5406e5a68f/images/sample.png -------------------------------------------------------------------------------- /lib/aws_config.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | 3 | class AwsConfig 4 | def initialize(opts={}) 5 | @opts = opts 6 | end 7 | 8 | def exclusions 9 | @exclusions ||=Exclusions.new(@opts[:exclude]) 10 | end 11 | 12 | def egress? 13 | @opts.key?(:egress) ? @opts[:egress] : true 14 | end 15 | 16 | def groups 17 | @opts[:groups] || {} 18 | end 19 | 20 | def format 21 | @opts[:format] || 'dot' 22 | end 23 | 24 | def debug? 25 | @opts[:debug] || false 26 | end 27 | 28 | def obfuscate? 29 | @opts[:obfuscate] || false 30 | end 31 | 32 | def self.load(file) 33 | config_opts = File.exist?(file) ? YAML.load_file(file) : {} 34 | AwsConfig.new(config_opts) 35 | end 36 | 37 | def merge(opts) 38 | AwsConfig.new(@opts.merge!(opts)) 39 | end 40 | 41 | def self.write(file) 42 | FileUtils.cp(File.expand_path('../opts.yml.sample', __FILE__), file) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/aws_security_viz.rb: -------------------------------------------------------------------------------- 1 | require_relative 'ec2/security_groups' 2 | require_relative 'provider/json' 3 | require_relative 'provider/ec2' 4 | require_relative 'renderer/all' 5 | require_relative 'graph' 6 | require_relative 'graph_filter' 7 | require_relative 'exclusions' 8 | require_relative 'debug_graph' 9 | require_relative 'color_picker' 10 | require_relative 'aws_config' 11 | 12 | class VisualizeAws 13 | def initialize(config, options={}) 14 | @options = options 15 | @config = config 16 | provider = options[:source_file].nil? ? Ec2Provider.new(options) : JsonProvider.new(options) 17 | @security_groups = SecurityGroups.new(provider, config) 18 | end 19 | 20 | def unleash(output_file) 21 | g = build 22 | g.filter(@options[:source_filter], @options[:target_filter]) 23 | g.output(Renderer.pick(@options[:renderer], output_file, @config)) 24 | end 25 | 26 | def build 27 | g = @config.obfuscate? ? DebugGraph.new(@config) : Graph.new(@config) 28 | @security_groups.each_with_index { |group, index| 29 | picker = ColorPicker.new(@options[:color]) 30 | g.add_node(group.name, {vpc_id: group.vpc_id, group_id: group.group_id}) 31 | group.traffic.each { |traffic| 32 | if traffic.ingress 33 | g.add_edge(traffic.from, traffic.to, :color => picker.color(index, traffic.ingress), :label => traffic.port_range) 34 | else 35 | g.add_edge(traffic.to, traffic.from, :color => picker.color(index, traffic.ingress), :label => traffic.port_range) 36 | end 37 | } 38 | } 39 | g 40 | end 41 | 42 | end 43 | 44 | -------------------------------------------------------------------------------- /lib/color_picker.rb: -------------------------------------------------------------------------------- 1 | class ColorPicker 2 | def initialize(colored) 3 | @picker = colored ? NodeColors.new : DefaultColors.new 4 | end 5 | def color(index, ingress) 6 | @picker.color(index, ingress) 7 | end 8 | class NodeColors 9 | def color(index, ingress) 10 | ColorPicker::COLORS[index % ColorPicker::COLORS.length] 11 | end 12 | end 13 | class DefaultColors 14 | def color(index, ingress) 15 | ingress ? :blue : :red 16 | end 17 | end 18 | 19 | COLORS = %w( 20 | #00004c 21 | #000080 22 | #0000fb 23 | #003a52 24 | #0040cd 25 | #0050b2 26 | #005390 27 | #007800 28 | #007eff 29 | #0096D8 30 | #009917 31 | #00B4AB 32 | #00a6a6 33 | #00cafe 34 | #012456 35 | #0298c3 36 | #02f88c 37 | #04133b 38 | #0579aa 39 | #078193 40 | #0aa0ff 41 | #0db7ed 42 | #0e60e3 43 | #101F1F 44 | #118f9e 45 | #120F14 46 | #14253c 47 | #178600 48 | #185619 49 | #198CE7 50 | #199f4b 51 | #1C3552 52 | #1D222D 53 | #1F1F1F 54 | #1ac620 55 | #1e4aec 56 | #22228f 57 | #244776 58 | #28431f 59 | #2ACCA8 60 | #2F2530 61 | #2b7489 62 | #2c3e50 63 | #302B6D 64 | #315665 65 | #341708 66 | #348a34 67 | #3572A5 68 | #358a5b 69 | #375eab 70 | #37775b 71 | #3A4E3A 72 | #3D6117 73 | #3F3F3F 74 | #3F85AF 75 | #3ac486 76 | #3be133 77 | #3d3c6e 78 | #3d9970 79 | #3fb68b 80 | #403a40 81 | #40d47e 82 | #427819 83 | #42f1f4 84 | #438eff 85 | #447265 86 | #44a51c 87 | #46390b 88 | #499886 89 | #4A76B8 90 | #4B6BEF 91 | #4B6C4B 92 | #4C3023 93 | #4F5D95 94 | #4d41b1 95 | #5232e7 96 | #555555 97 | #563d7c 98 | #596706 99 | #5A8164 100 | #5B2063 101 | #5a6986 102 | #5c7611 103 | #5e5086 104 | #60B5CC 105 | #62A8D6 106 | #636746 107 | #646464 108 | #64C800 109 | #64b970 110 | #652B81 111 | #6594b9 112 | #6600cc 113 | #665a4e 114 | #6866fb 115 | #6E4C13 116 | #6a40fd 117 | #6c616e 118 | #6e4a7e 119 | #701516 120 | #7055b5 121 | #74283c 122 | #747faa 123 | #7582D1 124 | #776791 125 | #7790B2 126 | #77d9fb 127 | #79aa7a 128 | #7b9db4 129 | #7e7eff 130 | #7fa2a7 131 | #800000 132 | #814CCC 133 | #82937f 134 | #843179 135 | #878787 136 | #87AED7 137 | #882B0F 138 | #88562A 139 | #88ccff 140 | #89e051 141 | #8a1267 142 | #8dc63f 143 | #8f0f8d 144 | #8f14e9 145 | #8fb200 146 | #913960 147 | #945db7 148 | #946d57 149 | #94B0C7 150 | #990000 151 | #999999 152 | #99DA07 153 | #9DC3FF 154 | #9EEDFF 155 | #9d5200 156 | #A0AA87 157 | #AA6746 158 | #B0CE4E 159 | #B34936 160 | #B5314C 161 | #B83998 162 | #B9D9FF 163 | #C1F12E 164 | #C76F5B 165 | #C7D7DC 166 | #DA5B0B 167 | #DAE1C2 168 | #DBCA00 169 | #E3F171 170 | #E4E6F3 171 | #E6EFBB 172 | #E8274B 173 | #EB8CEB 174 | #F18E33 175 | #FEFE00 176 | #FF5000 177 | #FFF4F3 178 | #a270ba 179 | #a3522f 180 | #a54c4d 181 | #a78649 182 | #a9188d 183 | #a957b0 184 | #aa2afe 185 | #adb2cb 186 | #b07219 187 | #b0b77e 188 | #b2011d 189 | #b2b7f8 190 | #b30000 191 | #b7e1f4 192 | #b845fc 193 | #ba595e 194 | #c065db 195 | #c22d40 196 | #c4a79c 197 | #c7a938 198 | #c9df40 199 | #cabbff 200 | #cc0000 201 | #cc0088 202 | #cc9900 203 | #cca760 204 | #ccccff 205 | #ccce35 206 | #cd6400 207 | #cdd0e3 208 | #cf142b 209 | #d4bec1 210 | #d80074 211 | #da291c 212 | #dad8d8 213 | #db5855 214 | #db901e 215 | #dbb284 216 | #dc566d 217 | #dce200 218 | #dea584 219 | #df7900 220 | #dfa535 221 | #e16737 222 | #e34c26 223 | #e4cc98 224 | #e69f56 225 | #ecdebe 226 | #ed2cd6 227 | #f0a9f0 228 | #f1e05a 229 | #f34b7d 230 | #f3ca0a 231 | #f50000 232 | #f7ede0 233 | #f97732 234 | #fab738 235 | #fb855d 236 | #fbe5cd 237 | #fcd7de 238 | #ff0c5a 239 | #ff2b2b 240 | #ff6375 241 | #ff7f7f 242 | #ffac45 243 | #fffaa0 244 | ) 245 | end 246 | -------------------------------------------------------------------------------- /lib/debug/parse_log.rb: -------------------------------------------------------------------------------- 1 | require_relative '../graph.rb' 2 | require 'organic_hash' 3 | 4 | @oh = OrganicHash.new 5 | 6 | def h(s) 7 | @oh.hash(s) 8 | end 9 | 10 | 11 | def debug 12 | g = Graph.new 13 | File.readlines('debug-output.log').map do |l| 14 | type, left, right = l.split(/\W+/) 15 | if type=="node" 16 | g.add_node(h(left), {}) 17 | elsif type=="edge" 18 | g.add_edge(h(left), h(right), {}) 19 | end 20 | end 21 | g.output(:svg => 'test.svg', :use => 'sfdp') 22 | end 23 | 24 | if __FILE__ == $0 25 | debug 26 | end 27 | -------------------------------------------------------------------------------- /lib/debug_graph.rb: -------------------------------------------------------------------------------- 1 | require 'digest' 2 | require_relative 'graph' 3 | 4 | class DebugGraph 5 | def initialize(config) 6 | @g = Graph.new(config) 7 | end 8 | 9 | def add_node(name, opts) 10 | @g.add_node(h(name), opts) if name 11 | end 12 | 13 | def add_edge(from, to, opts) 14 | @g.add_edge(h(from), h(to), opts.update(label: h(opts[:label]))) 15 | end 16 | 17 | def filter(source, destination) 18 | @g.filter(source, destination) 19 | end 20 | 21 | def output(renderer) 22 | @g.output(renderer) 23 | end 24 | 25 | private 26 | def h(msg) 27 | Digest::SHA256.hexdigest msg 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/ec2/ip_permission.rb: -------------------------------------------------------------------------------- 1 | require_relative 'traffic.rb' 2 | 3 | class IpPermission 4 | def initialize(group, ip, ingress, exclusions) 5 | @group = group 6 | @ip = ip 7 | @ingress = ingress 8 | @exclusions = exclusions 9 | end 10 | 11 | def traffic 12 | cidr_traffic + group_traffic 13 | end 14 | 15 | private 16 | def port_range 17 | @ip.protocol == '-1' ? '*' : [@ip.from, @ip.to].uniq.join('-') + '/' + @ip.protocol 18 | end 19 | 20 | def cidr_traffic 21 | @ip.ip_ranges 22 | .select { |range| !@exclusions.match(range)} 23 | .collect { |range| 24 | Traffic.new(@ingress, range.cidr_ip, @group.name, port_range) 25 | } 26 | end 27 | 28 | def group_traffic 29 | @ip.groups 30 | .select { |gp| !@exclusions.match(gp.name)} 31 | .collect { |gp| 32 | Traffic.new(@ingress, gp.name, @group.name, port_range) 33 | } 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/ec2/security_groups.rb: -------------------------------------------------------------------------------- 1 | require 'set' 2 | require 'forwardable' 3 | require_relative 'ip_permission.rb' 4 | 5 | class SecurityGroups 6 | include Enumerable 7 | 8 | def initialize(provider, config) 9 | @groups = provider.security_groups 10 | @config = config 11 | end 12 | 13 | def each(&block) 14 | groups = @groups.select { |sg| !@config.exclusions.match(sg.name) } 15 | groups.each { |group| 16 | if block_given? 17 | block.call SecurityGroup.new(@groups, group, @config) 18 | else 19 | yield SecurityGroup.new(@groups, group, @config) 20 | end 21 | } 22 | end 23 | 24 | def size 25 | @groups.size 26 | end 27 | end 28 | 29 | class SecurityGroup 30 | extend Forwardable 31 | 32 | def_delegators :@group, :name, :vpc_id, :group_id 33 | 34 | def initialize(all_groups, group, config) 35 | @all_groups = all_groups 36 | @group = group 37 | @config = config 38 | end 39 | 40 | def permissions 41 | ingress_permissions = @group.ip_permissions.collect { |ip| 42 | IpPermission.new(@group, ip, true, @config.exclusions) 43 | } 44 | return ingress_permissions unless @config.egress? 45 | egress_permissions = @group.ip_permissions_egress.collect { |ip| 46 | IpPermission.new(@group, ip, false, @config.exclusions) 47 | } 48 | ingress_permissions + egress_permissions 49 | end 50 | 51 | def traffic 52 | all_traffic = permissions.collect { |permission| 53 | permission.traffic 54 | }.flatten.uniq 55 | CidrGroupMapping.new(@all_groups, @config.groups).map(all_traffic) 56 | end 57 | end 58 | 59 | class CidrGroupMapping 60 | def initialize(all_groups, user_groups) 61 | @all_groups = all_groups 62 | @user_groups = user_groups 63 | end 64 | 65 | def map(all_traffic) 66 | traffic = all_traffic.collect { |traffic| 67 | traffic.copy(mapping(traffic.from), mapping(traffic.to)) 68 | } 69 | traffic.uniq.group_by {|t| [t.from, t.to, t.ingress]}.collect {|k,v| Traffic.grouped(v)}.uniq 70 | end 71 | 72 | private 73 | def mapping(val) 74 | group = @all_groups.find { |g| g.group_id == val } 75 | name = group.nil? ? val : group.name 76 | @user_groups[name] ? @user_groups[name] : name 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/ec2/traffic.rb: -------------------------------------------------------------------------------- 1 | class Traffic 2 | attr_accessor :from, :to, :port_range, :ingress 3 | 4 | def initialize(ingress, from, to, port_range) 5 | @ingress = ingress 6 | @from = from 7 | @to = to 8 | @port_range = port_range 9 | end 10 | 11 | def copy(from, to) 12 | Traffic.new(@ingress, from, to, @port_range) 13 | end 14 | 15 | def eql?(other) 16 | if @ingress == other.ingress 17 | @from == other.from && @to == other.to && @port_range == other.port_range 18 | else 19 | @from == other.to && @to == other.from && @port_range == other.port_range 20 | end 21 | end 22 | 23 | def hash 24 | @from.hash + @to.hash + @port_range.hash 25 | end 26 | 27 | def self.grouped(traffic_list) 28 | t = traffic_list.first 29 | port_range = traffic_list.collect(&:port_range).uniq.join(',') 30 | Traffic.new(t.ingress, t.from, t.to, port_range) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/exclusions.rb: -------------------------------------------------------------------------------- 1 | class Exclusions 2 | attr_reader :patterns 3 | 4 | def initialize(patterns) 5 | @patterns = patterns.map {|p| /#{p}/} unless patterns.nil? 6 | end 7 | 8 | def match(str) 9 | return false if patterns.nil? 10 | patterns.any? { |p| 11 | p.match(str) 12 | } 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/export/html/navigator.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | GraphNavigator 6 | 7 | 8 | 10 | 12 | 14 | 16 | 27 | 28 | 29 | 30 | 31 |
32 |
33 |
34 | 35 |
36 | 37 |
39 |
40 |
41 | 42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | 50 |
52 |
53 | 54 |
55 |
56 | 57 |
58 |
59 | 60 |
61 |
62 |
63 | 64 |
65 |
66 |
67 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /lib/export/html/view.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 161 | 162 | 163 | 164 | -------------------------------------------------------------------------------- /lib/graph.rb: -------------------------------------------------------------------------------- 1 | require 'rgl/adjacency' 2 | 3 | class Graph 4 | attr_reader :underlying 5 | 6 | def initialize(config, underlying=RGL::DirectedAdjacencyGraph.new) 7 | @config = config 8 | @underlying = underlying 9 | @edge_properties = {} 10 | @node_properties = {} 11 | end 12 | 13 | def add_node(name, opts) 14 | log("node: #{name}, opts: #{opts}") 15 | @underlying.add_vertex(name) 16 | @node_properties[name] = opts 17 | end 18 | 19 | def add_edge(from, to, opts) 20 | log("edge: #{from} -> #{to}") 21 | @underlying.add_edge(from, to) 22 | @edge_properties[[from, to]] = opts 23 | end 24 | 25 | def filter(source, destination) 26 | @underlying = GraphFilter.new(underlying).filter(source, destination) 27 | end 28 | 29 | def output(renderer) 30 | @underlying.each_vertex { |v| renderer.add_node(v, @node_properties[v] || {}) } 31 | @underlying.each_edge { |u, v| 32 | renderer.add_edge(u, v, opts(u, v)) 33 | } 34 | renderer.output 35 | end 36 | 37 | def log(msg) 38 | puts msg if @config.debug? 39 | end 40 | 41 | private 42 | def opts(u, v) 43 | @edge_properties[[u, v]] 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/graph_filter.rb: -------------------------------------------------------------------------------- 1 | require 'rgl/traversal' 2 | require 'rgl/implicit' 3 | 4 | class GraphFilter 5 | def initialize(graph) 6 | @graph = graph 7 | end 8 | 9 | def filter(source, destination) 10 | return @graph if source.nil? && destination.nil? 11 | if !source.nil? && destination.nil? 12 | return reduce(@graph, source) 13 | end 14 | if !destination.nil? && source.nil? 15 | return reduce(@graph.reverse, destination).reverse 16 | end 17 | search(@graph, source, destination) 18 | end 19 | 20 | private 21 | def reduce(graph, source) 22 | tree = graph.bfs_search_tree_from(source) 23 | graph.vertices_filtered_by { |v| tree.has_vertex? v } 24 | end 25 | 26 | def search(graph, source, destination) 27 | visitor = RGL::DFSVisitor.new(graph) 28 | path = [] 29 | paths = [] 30 | visitor.set_examine_vertex_event_handler { |x| path << x } 31 | visitor.set_finish_vertex_event_handler { |x| path.pop } 32 | visitor.set_examine_edge_event_handler { |x, y| paths << path.clone + [y] if y == destination } 33 | graph.depth_first_visit(source, visitor) { |x|} 34 | to_remove = graph.vertices - paths.flatten 35 | graph.remove_vertices(*to_remove) 36 | graph 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/opts.yml.sample: -------------------------------------------------------------------------------- 1 | --- 2 | :format: dot 3 | #:egress: true 4 | #:exclude: 5 | #- .*test.* 6 | #- .*perf.* 7 | #- .*0.0.0.0/32.* 8 | :groups: 9 | # '0.0.0.0/0': '*' 10 | 'x.x.x.x/0': 'External' 11 | 'y.y.y.y/0': 'External' 12 | 13 | -------------------------------------------------------------------------------- /lib/provider/ec2.rb: -------------------------------------------------------------------------------- 1 | require 'aws-sdk-ec2' 2 | 3 | class Ec2Provider 4 | 5 | def initialize(options) 6 | @options = options 7 | conn_opts = { 8 | region: options[:region], 9 | access_key_id: options[:access_key], 10 | secret_access_key: options[:secret_key], 11 | session_token: options[:session_token] 12 | }.delete_if {|k,v| v.nil?} 13 | 14 | @client = Aws::EC2::Client.new(conn_opts) 15 | end 16 | 17 | def security_groups 18 | @client.describe_security_groups.security_groups.reject { |sg| 19 | @options[:vpc_id] && sg.vpc_id != @options[:vpc_id] 20 | }.collect { |sg| 21 | Ec2::SecurityGroup.new(sg) 22 | } 23 | end 24 | end 25 | 26 | module Ec2 27 | class SecurityGroup 28 | extend Forwardable 29 | def_delegators :@sg, :name, :group_id, :vpc_id 30 | def initialize(sg) 31 | @sg = sg 32 | end 33 | 34 | def name 35 | @sg.group_name 36 | end 37 | 38 | def ip_permissions 39 | @sg.ip_permissions.collect { |ip| 40 | Ec2::IpPermission.new(ip) 41 | } 42 | end 43 | 44 | def ip_permissions_egress 45 | @sg.ip_permissions_egress.collect { |ip| 46 | Ec2::IpPermission.new(ip) 47 | } 48 | end 49 | end 50 | 51 | class IpPermission 52 | def initialize(ip) 53 | @ip = ip 54 | end 55 | 56 | def protocol 57 | @ip['ip_protocol'] 58 | end 59 | 60 | def from 61 | @ip['from_port'] 62 | end 63 | 64 | def to 65 | @ip['to_port'] 66 | end 67 | 68 | def ip_ranges 69 | @ip['ip_ranges'].collect {|gp| 70 | Ec2::IpPermissionRange.new(gp) 71 | } 72 | end 73 | 74 | def groups 75 | @ip['user_id_group_pairs'].collect {|gp| 76 | Ec2::IpPermissionGroup.new(gp) 77 | } 78 | end 79 | end 80 | 81 | class IpPermissionRange 82 | def initialize(range) 83 | @range = range 84 | end 85 | 86 | def cidr_ip 87 | @range['cidr_ip'] 88 | end 89 | 90 | def to_str 91 | cidr_ip 92 | end 93 | end 94 | 95 | class IpPermissionGroup 96 | def initialize(gp) 97 | @gp = gp 98 | end 99 | 100 | def name 101 | @gp['group_name'] || @gp['group_id'] 102 | end 103 | end 104 | 105 | end 106 | -------------------------------------------------------------------------------- /lib/provider/json.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | class JsonProvider 4 | def initialize(options) 5 | @groups = JSON.parse(File.read(options[:source_file]))['SecurityGroups'] 6 | end 7 | 8 | def security_groups 9 | @groups.collect { |sg| 10 | Json::SecurityGroup.new(sg) 11 | } 12 | end 13 | end 14 | 15 | module Json 16 | class SecurityGroup 17 | def initialize(sg) 18 | @sg = sg 19 | end 20 | 21 | def name 22 | @sg['GroupName'] 23 | end 24 | 25 | def group_id 26 | @sg['GroupId'] 27 | end 28 | 29 | def vpc_id 30 | @sg['VpcId'] 31 | end 32 | 33 | def ip_permissions 34 | @sg['IpPermissions'].collect { |ip| 35 | Json::IpPermission.new(ip) 36 | } 37 | end 38 | 39 | def group_id 40 | @sg['GroupId'] 41 | end 42 | 43 | def ip_permissions_egress 44 | @sg['IpPermissionsEgress'].collect { |ip| 45 | Json::IpPermission.new(ip) 46 | } 47 | end 48 | end 49 | 50 | class IpPermission 51 | def initialize(ip) 52 | @ip = ip 53 | end 54 | 55 | def protocol 56 | @ip['IpProtocol'] 57 | end 58 | 59 | def from 60 | @ip['FromPort'] 61 | end 62 | 63 | def to 64 | @ip['ToPort'] 65 | end 66 | 67 | def ip_ranges 68 | @ip['IpRanges'].collect { |gp| 69 | Json::IpPermissionRange.new(gp) 70 | } 71 | end 72 | 73 | def groups 74 | @ip['UserIdGroupPairs'].collect { |pair| 75 | Json::IpPermissionGroup.new(pair) 76 | } 77 | end 78 | 79 | end 80 | 81 | class IpPermissionRange 82 | def initialize(range) 83 | @range = range 84 | end 85 | 86 | def cidr_ip 87 | @range['CidrIp'] 88 | end 89 | 90 | def to_str 91 | cidr_ip 92 | end 93 | end 94 | 95 | class IpPermissionGroup 96 | def initialize(gp) 97 | @gp = gp 98 | end 99 | 100 | def name 101 | @gp['GroupName'] || @gp['GroupId'] 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /lib/renderer/all.rb: -------------------------------------------------------------------------------- 1 | require_relative 'navigator' 2 | require_relative 'json' 3 | require_relative 'graphviz' 4 | module Renderer 5 | ALL = {graphviz: Renderer::GraphViz, json: Renderer::Json, navigator: Renderer::Navigator} 6 | def self.pick(r, output_file, config) 7 | (ALL[(r || 'graphviz').to_sym]).new(output_file, config) 8 | end 9 | 10 | def self.copy_asset(asset, file_name) 11 | FileUtils.copy(File.expand_path("../../export/html/#{asset}", __FILE__), 12 | File.expand_path(asset, @file_name)) 13 | end 14 | 15 | def self.all 16 | ALL.keys 17 | end 18 | end -------------------------------------------------------------------------------- /lib/renderer/graphviz.rb: -------------------------------------------------------------------------------- 1 | require 'graphviz' 2 | 3 | module Renderer 4 | class GraphViz 5 | def initialize(file_name, config) 6 | @g = Graphviz::Graph.new('G', **{ 7 | overlap: false, 8 | splines: true, 9 | sep: 1, 10 | concentrate: true, 11 | rankdir: "LR" 12 | }) 13 | @file_name = file_name 14 | @config = config 15 | end 16 | 17 | def add_node(name, opts) 18 | @g.add_node(name, label: name) 19 | end 20 | 21 | def add_edge(from, to, opts) 22 | from_node = create_if_missing(from) 23 | to_node = create_if_missing(to) 24 | options = ({style: 'bold'}).merge(opts) 25 | from_node.connect(to_node, options) 26 | end 27 | 28 | def create_if_missing(name) 29 | n = @g.get_node(name).first 30 | n.nil? ? add_node(name, {}) : n 31 | end 32 | 33 | def output 34 | Graphviz::output(@g, path: @file_name, format: nil) #format: nil to force detection based on extension. 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/renderer/json.rb: -------------------------------------------------------------------------------- 1 | module Renderer 2 | class Json 3 | def initialize(file_name, config) 4 | @nodes = [] 5 | @edges = [] 6 | @file_name = file_name 7 | @config = config 8 | end 9 | 10 | def add_node(name, opts) 11 | @nodes << {id: name, label: name} 12 | end 13 | 14 | def add_edge(from, to, opts) 15 | @edges << {id: "#{from}-#{to}", source: from, target: to, label: opts[:label]} 16 | end 17 | 18 | def output 19 | IO.write(@file_name, {nodes: @nodes, edges: @edges}.to_json) 20 | Renderer.copy_asset('view.html', @file_name) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/renderer/navigator.rb: -------------------------------------------------------------------------------- 1 | require 'set' 2 | 3 | module Renderer 4 | class Navigator 5 | def initialize(file_name, config) 6 | @nodes = [] 7 | @edges = [] 8 | @file_name = file_name 9 | @config = config 10 | @categories = Set.new() 11 | end 12 | 13 | def add_node(name, opts) 14 | vpc = opts[:vpc_id] || 'default' 15 | info = "Security group: #{name},
VPC: #{vpc}" 16 | @nodes << {id: name, label: name, categories: [vpc], info: info} 17 | @categories.add(vpc) 18 | end 19 | 20 | def add_edge(from, to, opts) 21 | @edges << {id: "#{from}-#{to}", from: from, to: to, label: opts[:label]} 22 | end 23 | 24 | def output 25 | IO.write(@file_name, { 26 | data: {nodes: @nodes, edges: @edges}, 27 | categories: Hash[@categories.map{|c| [c, c]}] 28 | }.to_json) 29 | Renderer.copy_asset('navigator.html', @file_name) 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/version.rb: -------------------------------------------------------------------------------- 1 | module AwsSecurityViz 2 | VERSION = '0.2.5' 3 | end 4 | -------------------------------------------------------------------------------- /spec/color_picker_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe ColorPicker do 4 | context 'default picker' do 5 | let(:picker) { ColorPicker.new(false) } 6 | 7 | it 'should add default colors for edges' do 8 | expect(picker.color(0, true)).to eq(:blue) 9 | expect(picker.color(0, false)).to eq(:red) 10 | end 11 | end 12 | context 'color picker' do 13 | let(:picker) { ColorPicker.new(true) } 14 | 15 | it 'should add default colors for edges' do 16 | expect(picker.color(0, 'ignore')).to eq('#00004c') 17 | expect(picker.color(10000, 'ignore')).to eq('#C76F5B') 18 | end 19 | end 20 | end -------------------------------------------------------------------------------- /spec/graph_filter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'ostruct' 3 | 4 | describe GraphFilter do 5 | it 'should include nodes reachable from source' do 6 | graph = GraphFilter.new(RGL::DirectedAdjacencyGraph[1, 2, 2, 3, 2, 4, 4, 5]) 7 | 8 | expect(graph.filter(2, nil).to_s).to eq('(2-3)(2-4)(4-5)') 9 | end 10 | it 'should remove nodes not reachable from source' do 11 | graph = GraphFilter.new(RGL::DirectedAdjacencyGraph[1, 2, 2, 3, 2, 4, 4, 5, 3, 5]) 12 | 13 | expect(graph.filter(3, nil).to_s).to eq('(3-5)') 14 | end 15 | it 'should remove nodes not reachable to destination' do 16 | graph = GraphFilter.new(RGL::DirectedAdjacencyGraph[ 17 | 1, 2, 18 | 1, 3, 19 | 1, 4, 20 | 2, 3, 21 | 2, 4, 22 | 3, 4, 23 | ]) 24 | 25 | expect(graph.filter(nil, 3).to_s).to eq('(1-2)(1-3)(2-3)') 26 | end 27 | it 'should remove nodes not reachable to destination from source' do 28 | graph = GraphFilter.new(RGL::DirectedAdjacencyGraph[ 29 | 1, 2, 30 | 1, 3, 31 | 1, 4, 32 | 2, 3, 33 | 2, 4, 34 | ]) 35 | 36 | expect(graph.filter(2, 4).to_s).to eq('(2-4)') 37 | end 38 | it 'should retain edges which pass through intermediate nodes' do 39 | graph = GraphFilter.new(RGL::DirectedAdjacencyGraph[ 40 | 1, 2, 41 | 1, 3, 42 | 1, 4, 43 | 2, 3, 44 | 2, 4, 45 | 3, 4, 46 | ]) 47 | expect(graph.filter(2, 4).to_s).to eq('(2-3)(2-4)(3-4)') 48 | end 49 | it 'should remove nodes not reachable to destination from source #1' do 50 | graph = GraphFilter.new(RGL::DirectedAdjacencyGraph[ 51 | 1, 2, 52 | 1, 3, 53 | 1, 4, 54 | 2, 3, 55 | 2, 4, 56 | 3, 5, 57 | 5, 4 58 | ]) 59 | expect(graph.filter(1, 5).to_s).to eq('(1-2)(1-3)(2-3)(3-5)') 60 | end 61 | it 'should remove nodes not reachable to destination from source #2' do 62 | graph = GraphFilter.new(RGL::DirectedAdjacencyGraph[ 63 | 1, 2, 64 | 1, 3, 65 | 1, 4, 66 | 2, 3, 67 | 2, 4, 68 | 3, 5, 69 | 5, 4 70 | ]) 71 | expect(graph.filter(1, 4).to_s).to eq('(1-2)(1-3)(1-4)(2-3)(2-4)(3-5)(5-4)') 72 | end 73 | it 'should remove nodes not reachable to destination from source #3' do 74 | graph = GraphFilter.new(RGL::DirectedAdjacencyGraph[ 75 | 1, 2, 76 | 1, 3, 77 | 1, 4, 78 | 2, 3, 79 | 2, 4, 80 | 3, 5, 81 | 5, 4 82 | ]) 83 | expect(graph.filter(2, 4).to_s).to eq('(2-3)(2-4)(3-5)(5-4)') 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /spec/integration/aws_expected.json: -------------------------------------------------------------------------------- 1 | {"nodes":[{"id":"asv-app","label":"asv-app"},{"id":"asv-bastion","label":"asv-bastion"},{"id":"*","label":"*"},{"id":"asv-db","label":"asv-db"},{"id":"asv-solr","label":"asv-solr"},{"id":"default","label":"default"}],"edges":[{"id":"asv-bastion-asv-app","source":"asv-bastion","target":"asv-app","label":"22/tcp"},{"id":"*-asv-app","source":"*","target":"asv-app","label":"80/tcp"},{"id":"*-asv-bastion","source":"*","target":"asv-bastion","label":"22/tcp"},{"id":"asv-app-asv-db","source":"asv-app","target":"asv-db","label":"5432/tcp"},{"id":"asv-bastion-asv-db","source":"asv-bastion","target":"asv-db","label":"22/tcp"},{"id":"asv-app-asv-solr","source":"asv-app","target":"asv-solr","label":"8983/tcp"},{"id":"asv-bastion-asv-solr","source":"asv-bastion","target":"asv-solr","label":"22/tcp"},{"id":"default-default","source":"default","target":"default","label":"*"},{"id":"*-default","source":"*","target":"default","label":"22/tcp"},{"id":"default-*","source":"default","target":"*","label":"*"}]} -------------------------------------------------------------------------------- /spec/integration/dummy.dot: -------------------------------------------------------------------------------- 1 | digraph G { 2 | graph [bb="0,0,388.31,144", 3 | concentrate=true, 4 | overlap=false, 5 | rankdir=LR, 6 | sep=1, 7 | splines=true 8 | ]; 9 | node [label="\N"]; 10 | app [height=0.5, 11 | label=app, 12 | pos="222.32,72", 13 | width=0.75]; 14 | db [height=0.5, 15 | label=db, 16 | pos="361.31,72", 17 | width=0.75]; 18 | app -> db [color=blue, 19 | label="5984/tcp", 20 | lp="291.81,79", 21 | pos="e,334.25,72 249.45,72 270.68,72 300.46,72 323.95,72", 22 | style=bold]; 23 | "8.8.8.8/32" [height=0.5, 24 | label="8.8.8.8/32", 25 | pos="62.161,126", 26 | width=1.2702]; 27 | "8.8.8.8/32" -> app [color=blue, 28 | label="80/tcp", 29 | lp="159.82,108", 30 | pos="e,199.09,81.689 98.964,115.29 121.69,108.48 151.36,99.231 177.32,90 181.33,88.572 185.52,87.006 189.65,85.41", 31 | style=bold]; 32 | "amazon-elb-sg" [height=0.5, 33 | label="amazon-elb-sg", 34 | pos="62.161,72", 35 | width=1.7267]; 36 | "amazon-elb-sg" -> app [color=blue, 37 | label="80/tcp", 38 | lp="159.82,79", 39 | pos="e,195.01,72 124.54,72 144.95,72 166.98,72 184.95,72", 40 | style=bold]; 41 | "*" [height=0.5, 42 | label="*", 43 | pos="62.161,18", 44 | width=0.75]; 45 | "*" -> app [color=blue, 46 | label="22/tcp", 47 | lp="159.82,61", 48 | pos="e,199.09,62.311 87.055,25.175 110.5,32.076 146.53,43.049 177.32,54 181.33,55.428 185.52,56.994 189.65,58.59", 49 | style=bold]; 50 | } 51 | -------------------------------------------------------------------------------- /spec/integration/dummy.json: -------------------------------------------------------------------------------- 1 | { 2 | "SecurityGroups": [ 3 | { 4 | "IpPermissionsEgress": [], 5 | "Description": "app", 6 | "IpPermissions": [ 7 | { 8 | "ToPort": 80, 9 | "IpProtocol": "tcp", 10 | "IpRanges": [ 11 | { 12 | "CidrIp": "8.8.8.8/32" 13 | } 14 | ], 15 | "UserIdGroupPairs": [ 16 | { 17 | "GroupName": "amazon-elb-sg", 18 | "UserId": "amazon-elb", 19 | "GroupId": "sg-amzelb" 20 | } 21 | ], 22 | "FromPort": 80 23 | }, 24 | { 25 | "ToPort": 22, 26 | "IpProtocol": "tcp", 27 | "IpRanges": [ 28 | { 29 | "CidrIp": "0.0.0.0/0" 30 | } 31 | ], 32 | "UserIdGroupPairs": [], 33 | "FromPort": 22 34 | } 35 | ], 36 | "GroupName": "app", 37 | "OwnerId": "owner1", 38 | "GroupId": "sg-appgrp" 39 | }, 40 | { 41 | "IpPermissionsEgress": [], 42 | "Description": "db", 43 | "IpPermissions": [ 44 | { 45 | "ToPort": 5984, 46 | "IpProtocol": "tcp", 47 | "IpRanges": [], 48 | "UserIdGroupPairs": [ 49 | { 50 | "GroupName": "app", 51 | "UserId": "owner1", 52 | "GroupId": "sg-appgrp" 53 | } 54 | ], 55 | "FromPort": 5984 56 | } 57 | ], 58 | "GroupName": "db", 59 | "OwnerId": "owner1", 60 | "GroupId": "sg-dbgrp" 61 | } 62 | ] 63 | } 64 | 65 | -------------------------------------------------------------------------------- /spec/integration/expected.json: -------------------------------------------------------------------------------- 1 | {"nodes":[{"id":"app","label":"app"},{"id":"8.8.8.8/32","label":"8.8.8.8/32"},{"id":"amazon-elb-sg","label":"amazon-elb-sg"},{"id":"*","label":"*"},{"id":"db","label":"db"}],"edges":[{"id":"app-db","source":"app","target":"db","label":"5984/tcp"},{"id":"8.8.8.8/32-app","source":"8.8.8.8/32","target":"app","label":"80/tcp"},{"id":"amazon-elb-sg-app","source":"amazon-elb-sg","target":"app","label":"80/tcp"},{"id":"*-app","source":"*","target":"app","label":"22/tcp"}]} -------------------------------------------------------------------------------- /spec/integration/navigator.json: -------------------------------------------------------------------------------- 1 | {"data":{"nodes":[{"id":"app","label":"app","categories":["default"],"info":"Security group: app,
VPC: default"},{"id":"8.8.8.8/32","label":"8.8.8.8/32","categories":["default"],"info":"Security group: 8.8.8.8/32,
VPC: default"},{"id":"amazon-elb-sg","label":"amazon-elb-sg","categories":["default"],"info":"Security group: amazon-elb-sg,
VPC: default"},{"id":"*","label":"*","categories":["default"],"info":"Security group: *,
VPC: default"},{"id":"db","label":"db","categories":["default"],"info":"Security group: db,
VPC: default"}],"edges":[{"id":"app-db","from":"app","to":"db","label":"5984/tcp"},{"id":"8.8.8.8/32-app","from":"8.8.8.8/32","to":"app","label":"80/tcp"},{"id":"amazon-elb-sg-app","from":"amazon-elb-sg","to":"app","label":"80/tcp"},{"id":"*-app","from":"*","to":"app","label":"22/tcp"}]},"categories":{"default":"default"}} 2 | -------------------------------------------------------------------------------- /spec/integration/visualize_aws_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tempfile' 3 | 4 | RSpec::Matchers.define :be_graph_with do |nodes| 5 | match do |graphv| 6 | graphv.nodes.keys == nodes 7 | end 8 | end 9 | 10 | describe VisualizeAws do 11 | let(:opts) { 12 | { 13 | :source_file => source_file, 14 | :filename => temp_file 15 | } 16 | } 17 | let(:source_file) { File.join(File.dirname(__FILE__), 'dummy.json') } 18 | let(:config) { AwsConfig.new({groups: {'0.0.0.0/0' => '*'}}) } 19 | let(:expected_content) { File.read(expected_file) } 20 | let(:actual_content) { temp_file.read } 21 | 22 | context 'json to dot file' do 23 | let(:expected_file) { File.join(File.dirname(__FILE__), 'dummy.dot') } 24 | let(:temp_file) { Tempfile.new(%w(aws .dot)) } 25 | 26 | def without_pos(data) 27 | return data 28 | .gsub(/pos=\"[a-z,0-9\.\s]*\"/, '') 29 | .gsub(/\s{2,}/, ' ') # trim spaces 30 | .gsub(/\t/, ' ') # remove tabs 31 | end 32 | 33 | it 'should parse json input', :integration => true do 34 | VisualizeAws.new(config, opts).unleash(temp_file.path) 35 | expect(without_pos(expected_content)).to eq(without_pos(actual_content)) 36 | end 37 | 38 | it 'should parse json input with stubbed out graphviz' do 39 | nodes = ["app", "8.8.8.8/32", "amazon-elb-sg", "*", "db"] 40 | allow(Graphviz).to receive(:output).with(be_graph_with(nodes), path: temp_file.path, format: nil) 41 | VisualizeAws.new(config, opts).unleash(temp_file.path) 42 | end 43 | end 44 | 45 | context 'json to json file' do 46 | let(:expected_file) { File.join(File.dirname(__FILE__), 'expected.json') } 47 | let(:temp_file) { Tempfile.new(%w(aws .json)) } 48 | 49 | it 'should parse json input' do 50 | expect(FileUtils).to receive(:copy) 51 | VisualizeAws.new(config, opts.merge(:renderer => 'json')).unleash(temp_file.path) 52 | expect(JSON.parse(expected_content)).to eq(JSON.parse(actual_content)) 53 | end 54 | 55 | it 'should parse json input with obfuscation' do 56 | config = AwsConfig.new({groups: {'0.0.0.0/0' => '*'}, obfuscate: true}) 57 | expect(FileUtils).to receive(:copy) 58 | VisualizeAws.new(config, opts.merge(:renderer => 'json')).unleash(temp_file.path) 59 | expect(actual_content).not_to include('"amazon-elb-sg"', '"app"', '"db"') 60 | end 61 | 62 | end 63 | 64 | context 'json to navigator file' do 65 | let(:expected_file) { File.join(File.dirname(__FILE__), 'navigator.json') } 66 | let(:temp_file) { Tempfile.new(%w(aws .json)) } 67 | 68 | it 'should parse json input' do 69 | expect(FileUtils).to receive(:copy) 70 | VisualizeAws.new(config, opts.merge(:renderer => 'navigator')).unleash(temp_file.path) 71 | expect(JSON.parse(expected_content)).to eq(JSON.parse(actual_content)) 72 | end 73 | end 74 | 75 | if ENV['TEST_ACCESS_KEY'] 76 | context 'ec2 to json file' do 77 | let(:expected_file) { File.join(File.dirname(__FILE__), 'aws_expected.json') } 78 | let(:temp_file) { Tempfile.new(%w(aws .json)) } 79 | let(:opts) { 80 | { 81 | :filename => temp_file, 82 | :secret_key => ENV['TEST_SECRET_KEY'], 83 | :access_key => ENV['TEST_ACCESS_KEY'], 84 | :region => 'us-east-1' 85 | } 86 | } 87 | 88 | it 'should read from ec2 account', :integration => true do 89 | expect(FileUtils).to receive(:copy) 90 | VisualizeAws.new(config, opts).unleash(temp_file.path) 91 | expect(JSON.parse(expected_content)['edges']).to match_array(JSON.parse(actual_content)['edges']) 92 | expect(JSON.parse(expected_content)['nodes']).to match_array(JSON.parse(actual_content)['nodes']) 93 | end 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | if ENV['COVERAGE'] 2 | require 'simplecov' 3 | 4 | SimpleCov.start do 5 | add_filter '/spec/' 6 | end 7 | end 8 | 9 | require 'bundler' 10 | Bundler.require 11 | require 'rspec' 12 | require 'rubygems' 13 | require File.expand_path(File.dirname(__FILE__) + "/../config/boot") 14 | Dir[File.dirname(__FILE__) + "/support/**/*.rb"].each {|f| require f} 15 | 16 | def group name, *ingress 17 | {group_name: name, group_id: 'some group', ip_permissions: ingress, ip_permissions_egress: []} 18 | end 19 | 20 | def group_ingress port, name 21 | {user_id_group_pairs:[{user_id: "userId", group_id: "sg-groupId", group_name: name}], ip_ranges:[], ip_protocol: "tcp", from_port: port, to_port: port} 22 | end 23 | 24 | def cidr_ingress port, cidr_ip 25 | {ip_ranges:[{cidr_ip: cidr_ip}], ip_protocol: "tcp", from_port: port, to_port: port} 26 | end 27 | 28 | def stub_security_groups groups 29 | Aws.config[:ec2] = { 30 | stub_responses: { 31 | describe_security_groups: { 32 | security_groups: groups 33 | } 34 | } 35 | } 36 | end -------------------------------------------------------------------------------- /spec/visualize_aws_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class DummyRenderer 4 | attr_reader :output 5 | 6 | def initialize 7 | @output = [] 8 | end 9 | 10 | def add_node(name, opts) 11 | @output << [:node, name] 12 | end 13 | 14 | def add_edge(from, to, opts) 15 | @output << [:edge, from, to, opts] 16 | end 17 | end 18 | 19 | describe VisualizeAws do 20 | let(:visualize_aws) { VisualizeAws.new(AwsConfig.new) } 21 | let(:renderer) { DummyRenderer.new } 22 | 23 | it 'should add nodes, edges for each security group' do 24 | stub_security_groups([group('Remote ssh', group_ingress(22, 'My machine')), group('My machine')]) 25 | graph = visualize_aws.build 26 | 27 | expect(graph.output(renderer)).to contain_exactly( 28 | [:node, 'Remote ssh'], 29 | [:node, 'My machine'], 30 | [:edge, 'My machine', 'Remote ssh', {:color => :blue, :label => '22/tcp'}], 31 | ) 32 | end 33 | 34 | context 'groups' do 35 | it 'should add nodes for external security groups defined through ingress' do 36 | stub_security_groups([group('Web', group_ingress(80, 'ELB'))]) 37 | graph = visualize_aws.build 38 | 39 | expect(graph.output(renderer)).to contain_exactly( 40 | [:node, 'Web'], 41 | [:node, 'ELB'], 42 | [:edge, 'ELB', 'Web', {:color => :blue, :label => '80/tcp'}] 43 | ) 44 | end 45 | 46 | it 'should add an edge for each security ingress' do 47 | stub_security_groups( 48 | [ 49 | group('App', group_ingress(80, 'Web'), group_ingress(8983, 'Internal')), 50 | group('Web', group_ingress(80, 'External')), 51 | group('Db', group_ingress(7474, 'App')) 52 | ]) 53 | graph = visualize_aws.build 54 | 55 | expect(graph.output(renderer)).to contain_exactly( 56 | [:node, 'App'], 57 | [:node, 'Web'], 58 | [:edge, 'Web', 'App', {:color => :blue, :label => '80/tcp'}], 59 | [:node, 'Internal'], 60 | [:edge, 'Internal', 'App', {:color => :blue, :label => '8983/tcp'}], 61 | [:node, 'External'], 62 | [:edge, 'External', 'Web', {:color => :blue, :label => '80/tcp'}], 63 | [:node, 'Db'], 64 | [:edge, 'App', 'Db', {:color => :blue, :label => '7474/tcp'}] 65 | ) 66 | 67 | end 68 | end 69 | 70 | context 'cidr' do 71 | 72 | it 'should add an edge for each cidr ingress' do 73 | stub_security_groups( 74 | [ 75 | group('Web', group_ingress(80, 'External')), 76 | group('Db', group_ingress(7474, 'App'), cidr_ingress(22, '127.0.0.1/32')) 77 | ]) 78 | graph = visualize_aws.build 79 | 80 | expect(graph.output(renderer)).to contain_exactly( 81 | [:node, 'Web'], 82 | [:node, 'External'], 83 | [:edge, 'External', 'Web', {:color => :blue, :label => '80/tcp'}], 84 | [:node, 'Db'], 85 | [:node, 'App'], 86 | [:edge, 'App', 'Db', {:color => :blue, :label => '7474/tcp'}], 87 | [:node, '127.0.0.1/32'], 88 | [:edge, '127.0.0.1/32', 'Db', {:color => :blue, :label => '22/tcp'}] 89 | ) 90 | 91 | end 92 | 93 | it 'should add map edges for cidr ingress' do 94 | stub_security_groups( 95 | [ 96 | group('Web', group_ingress(80, 'External')), 97 | group('Db', group_ingress(7474, 'App'), cidr_ingress(22, '127.0.0.1/32')) 98 | ]) 99 | mapping = {'127.0.0.1/32' => 'Work'} 100 | mapping = CidrGroupMapping.new([], mapping) 101 | allow(CidrGroupMapping).to receive(:new).and_return(mapping) 102 | 103 | graph = visualize_aws.build 104 | 105 | expect(graph.output(renderer)).to contain_exactly( 106 | [:node, 'Web'], 107 | [:node, 'External'], 108 | [:edge, 'External', 'Web', {:color => :blue, :label => '80/tcp'}], 109 | [:node, 'Db'], 110 | [:node, 'App'], 111 | [:edge, 'App', 'Db', {:color => :blue, :label => '7474/tcp'}], 112 | [:node, 'Work'], 113 | [:edge, 'Work', 'Db', {:color => :blue, :label => '22/tcp'}] 114 | ) 115 | 116 | end 117 | 118 | it 'should group mapped duplicate edges for cidr ingress' do 119 | stub_security_groups( 120 | [ 121 | group('ssh', cidr_ingress(22, '192.168.0.1/32'), cidr_ingress(22, '127.0.0.1/32')) 122 | ]) 123 | mapping = {'127.0.0.1/32' => 'Work', '192.168.0.1/32' => 'Work'} 124 | mapping = CidrGroupMapping.new([], mapping) 125 | allow(CidrGroupMapping).to receive(:new).and_return(mapping) 126 | 127 | graph = visualize_aws.build 128 | 129 | expect(graph.output(renderer)).to contain_exactly( 130 | [:node, 'ssh'], 131 | [:node, 'Work'], 132 | [:edge, 'Work', 'ssh', {:color => :blue, :label => '22/tcp'}] 133 | ) 134 | end 135 | end 136 | 137 | context "filter" do 138 | it 'include cidr which do not match the pattern' do 139 | stub_security_groups( 140 | [ 141 | group('Web', cidr_ingress(22, '127.0.0.1/32')), 142 | group('Db', cidr_ingress(22, '192.0.1.1/32')) 143 | ]) 144 | 145 | opts = {:exclude => ['127.*']} 146 | graph = VisualizeAws.new(AwsConfig.new(opts)).build 147 | 148 | expect(graph.output(renderer)).to contain_exactly( 149 | [:node, 'Web'], 150 | [:node, 'Db'], 151 | [:node, '192.0.1.1/32'], 152 | [:edge, '192.0.1.1/32', 'Db', {:color => :blue, :label => '22/tcp'}] 153 | ) 154 | end 155 | 156 | it 'include groups which do not match the pattern' do 157 | stub_security_groups( 158 | [ 159 | group('Web', group_ingress(80, 'External')), 160 | group('Db', group_ingress(7474, 'App'), cidr_ingress(22, '127.0.0.1/32')) 161 | ]) 162 | 163 | opts = {:exclude => ['D.*b', 'App']} 164 | graph = VisualizeAws.new(AwsConfig.new(opts)).build 165 | 166 | expect(graph.output(renderer)).to contain_exactly( 167 | [:node, 'Web'], 168 | [:node, 'External'], 169 | [:edge, 'External', 'Web', {:color => :blue, :label => '80/tcp'}] 170 | ) 171 | end 172 | 173 | it 'include derived groups which do not match the pattern' do 174 | stub_security_groups( 175 | [ 176 | group('Web', group_ingress(80, 'External')), 177 | group('Db', group_ingress(7474, 'App'), cidr_ingress(22, '127.0.0.1/32')) 178 | ]) 179 | 180 | opts = {:exclude => ['App']} 181 | graph = VisualizeAws.new(AwsConfig.new(opts)).build 182 | 183 | expect(graph.output(renderer)).to contain_exactly( 184 | [:node, 'Web'], 185 | [:node, 'External'], 186 | [:edge, 'External', 'Web', {:color => :blue, :label => '80/tcp'}], 187 | [:node, 'Db'], 188 | [:node, '127.0.0.1/32'], 189 | [:edge, '127.0.0.1/32', 'Db', {:color => :blue, :label => '22/tcp'}] 190 | ) 191 | 192 | end 193 | end 194 | end 195 | --------------------------------------------------------------------------------