├── Dockerfile ├── LICENSE ├── README.md ├── bin ├── map-webserver └── map_network_sgs ├── cpanfile ├── examples └── graph1.png ├── icons ├── alb.png ├── asg-left.png ├── asg-right.png ├── elb.png ├── i.png ├── internet.png ├── memcached.png ├── network.png ├── rds.png ├── redis.png ├── redshift.png └── security_group.png ├── lib └── AWS │ └── Network │ └── SecurityGroupMap.pm └── t └── test_1.t /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.7 2 | 3 | WORKDIR /root 4 | 5 | ENTRYPOINT perl -I /root/lib/ /root/bin/map_network_sgs 6 | CMD [ 'eu-west-1' ] 7 | 8 | COPY . /root 9 | 10 | RUN apk update \ 11 | && apk add --no-cache curl wget make gcc musl-dev perl-dev graphviz-dev\ 12 | && apk add --no-cache perl-net-ssleay perl-xml-simple perl-moose perl-config-inifiles perl-getopt-long perl-data-compare perl-datetime perl-json-maybexs perl-path-tiny perl-dbi perl-date-simple \ 13 | && curl -LO http://www.cpan.org/authors/id/M/MI/MIYAGAWA/App-cpanminus-1.7043.tar.gz \ 14 | && echo '68a06f7da80882a95bc02c92c7ee305846fb6ab648cf83678ea945e44ad65c65 *App-cpanminus-1.7043.tar.gz' | sha256sum -c - \ 15 | && tar -xzf App-cpanminus-1.7043.tar.gz \ 16 | && cd App-cpanminus-1.7043 \ 17 | && perl bin/cpanm . \ 18 | && cd /root \ 19 | && cpanm -n --installdeps . \ 20 | && cpanm -n Devel::OverloadInfo \ 21 | && apk del make gcc musl-dev perl-dev \ 22 | && rm -fr cpanm /root/.cpanm /tmp/* App-cpanminus-1.7043* 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is Copyright (c) 2018 by CAPSiDE 2 | 3 | This is free software, licensed under: 4 | 5 | The Apache License, Version 2.0, January 2004 6 | 7 | Apache License 8 | Version 2.0, January 2004 9 | http://www.apache.org/licenses/ 10 | 11 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 12 | 13 | 1. Definitions. 14 | 15 | "License" shall mean the terms and conditions for use, reproduction, 16 | and distribution as defined by Sections 1 through 9 of this document. 17 | 18 | "Licensor" shall mean the copyright owner or entity authorized by 19 | the copyright owner that is granting the License. 20 | 21 | "Legal Entity" shall mean the union of the acting entity and all 22 | other entities that control, are controlled by, or are under common 23 | control with that entity. For the purposes of this definition, 24 | "control" means (i) the power, direct or indirect, to cause the 25 | direction or management of such entity, whether by contract or 26 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 27 | outstanding shares, or (iii) beneficial ownership of such entity. 28 | 29 | "You" (or "Your") shall mean an individual or Legal Entity 30 | exercising permissions granted by this License. 31 | 32 | "Source" form shall mean the preferred form for making modifications, 33 | including but not limited to software source code, documentation 34 | source, and configuration files. 35 | 36 | "Object" form shall mean any form resulting from mechanical 37 | transformation or translation of a Source form, including but 38 | not limited to compiled object code, generated documentation, 39 | and conversions to other media types. 40 | 41 | "Work" shall mean the work of authorship, whether in Source or 42 | Object form, made available under the License, as indicated by a 43 | copyright notice that is included in or attached to the work 44 | (an example is provided in the Appendix below). 45 | 46 | "Derivative Works" shall mean any work, whether in Source or Object 47 | form, that is based on (or derived from) the Work and for which the 48 | editorial revisions, annotations, elaborations, or other modifications 49 | represent, as a whole, an original work of authorship. For the purposes 50 | of this License, Derivative Works shall not include works that remain 51 | separable from, or merely link (or bind by name) to the interfaces of, 52 | the Work and Derivative Works thereof. 53 | 54 | "Contribution" shall mean any work of authorship, including 55 | the original version of the Work and any modifications or additions 56 | to that Work or Derivative Works thereof, that is intentionally 57 | submitted to Licensor for inclusion in the Work by the copyright owner 58 | or by an individual or Legal Entity authorized to submit on behalf of 59 | the copyright owner. For the purposes of this definition, "submitted" 60 | means any form of electronic, verbal, or written communication sent 61 | to the Licensor or its representatives, including but not limited to 62 | communication on electronic mailing lists, source code control systems, 63 | and issue tracking systems that are managed by, or on behalf of, the 64 | Licensor for the purpose of discussing and improving the Work, but 65 | excluding communication that is conspicuously marked or otherwise 66 | designated in writing by the copyright owner as "Not a Contribution." 67 | 68 | "Contributor" shall mean Licensor and any individual or Legal Entity 69 | on behalf of whom a Contribution has been received by Licensor and 70 | subsequently incorporated within the Work. 71 | 72 | 2. Grant of Copyright License. Subject to the terms and conditions of 73 | this License, each Contributor hereby grants to You a perpetual, 74 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 75 | copyright license to reproduce, prepare Derivative Works of, 76 | publicly display, publicly perform, sublicense, and distribute the 77 | Work and such Derivative Works in Source or Object form. 78 | 79 | 3. Grant of Patent License. Subject to the terms and conditions of 80 | this License, each Contributor hereby grants to You a perpetual, 81 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 82 | (except as stated in this section) patent license to make, have made, 83 | use, offer to sell, sell, import, and otherwise transfer the Work, 84 | where such license applies only to those patent claims licensable 85 | by such Contributor that are necessarily infringed by their 86 | Contribution(s) alone or by combination of their Contribution(s) 87 | with the Work to which such Contribution(s) was submitted. If You 88 | institute patent litigation against any entity (including a 89 | cross-claim or counterclaim in a lawsuit) alleging that the Work 90 | or a Contribution incorporated within the Work constitutes direct 91 | or contributory patent infringement, then any patent licenses 92 | granted to You under this License for that Work shall terminate 93 | as of the date such litigation is filed. 94 | 95 | 4. Redistribution. You may reproduce and distribute copies of the 96 | Work or Derivative Works thereof in any medium, with or without 97 | modifications, and in Source or Object form, provided that You 98 | meet the following conditions: 99 | 100 | (a) You must give any other recipients of the Work or 101 | Derivative Works a copy of this License; and 102 | 103 | (b) You must cause any modified files to carry prominent notices 104 | stating that You changed the files; and 105 | 106 | (c) You must retain, in the Source form of any Derivative Works 107 | that You distribute, all copyright, patent, trademark, and 108 | attribution notices from the Source form of the Work, 109 | excluding those notices that do not pertain to any part of 110 | the Derivative Works; and 111 | 112 | (d) If the Work includes a "NOTICE" text file as part of its 113 | distribution, then any Derivative Works that You distribute must 114 | include a readable copy of the attribution notices contained 115 | within such NOTICE file, excluding those notices that do not 116 | pertain to any part of the Derivative Works, in at least one 117 | of the following places: within a NOTICE text file distributed 118 | as part of the Derivative Works; within the Source form or 119 | documentation, if provided along with the Derivative Works; or, 120 | within a display generated by the Derivative Works, if and 121 | wherever such third-party notices normally appear. The contents 122 | of the NOTICE file are for informational purposes only and 123 | do not modify the License. You may add Your own attribution 124 | notices within Derivative Works that You distribute, alongside 125 | or as an addendum to the NOTICE text from the Work, provided 126 | that such additional attribution notices cannot be construed 127 | as modifying the License. 128 | 129 | You may add Your own copyright statement to Your modifications and 130 | may provide additional or different license terms and conditions 131 | for use, reproduction, or distribution of Your modifications, or 132 | for any such Derivative Works as a whole, provided Your use, 133 | reproduction, and distribution of the Work otherwise complies with 134 | the conditions stated in this License. 135 | 136 | 5. Submission of Contributions. Unless You explicitly state otherwise, 137 | any Contribution intentionally submitted for inclusion in the Work 138 | by You to the Licensor shall be under the terms and conditions of 139 | this License, without any additional terms or conditions. 140 | Notwithstanding the above, nothing herein shall supersede or modify 141 | the terms of any separate license agreement you may have executed 142 | with Licensor regarding such Contributions. 143 | 144 | 6. Trademarks. This License does not grant permission to use the trade 145 | names, trademarks, service marks, or product names of the Licensor, 146 | except as required for reasonable and customary use in describing the 147 | origin of the Work and reproducing the content of the NOTICE file. 148 | 149 | 7. Disclaimer of Warranty. Unless required by applicable law or 150 | agreed to in writing, Licensor provides the Work (and each 151 | Contributor provides its Contributions) on an "AS IS" BASIS, 152 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 153 | implied, including, without limitation, any warranties or conditions 154 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 155 | PARTICULAR PURPOSE. You are solely responsible for determining the 156 | appropriateness of using or redistributing the Work and assume any 157 | risks associated with Your exercise of permissions under this License. 158 | 159 | 8. Limitation of Liability. In no event and under no legal theory, 160 | whether in tort (including negligence), contract, or otherwise, 161 | unless required by applicable law (such as deliberate and grossly 162 | negligent acts) or agreed to in writing, shall any Contributor be 163 | liable to You for damages, including any direct, indirect, special, 164 | incidental, or consequential damages of any character arising as a 165 | result of this License or out of the use or inability to use the 166 | Work (including but not limited to damages for loss of goodwill, 167 | work stoppage, computer failure or malfunction, or any and all 168 | other commercial damages or losses), even if such Contributor 169 | has been advised of the possibility of such damages. 170 | 171 | 9. Accepting Warranty or Additional Liability. While redistributing 172 | the Work or Derivative Works thereof, You may choose to offer, 173 | and charge a fee for, acceptance of support, warranty, indemnity, 174 | or other liability obligations and/or rights consistent with this 175 | License. However, in accepting such obligations, You may act only 176 | on Your own behalf and on Your sole responsibility, not on behalf 177 | of any other Contributor, and only if You agree to indemnify, 178 | defend, and hold each Contributor harmless for any liability 179 | incurred by, or claims asserted against, such Contributor by reason 180 | of your accepting any such warranty or additional liability. 181 | 182 | END OF TERMS AND CONDITIONS 183 | 184 | APPENDIX: How to apply the Apache License to your work. 185 | 186 | To apply the Apache License to your work, attach the following 187 | boilerplate notice, with the fields enclosed by brackets "[]" 188 | replaced with your own identifying information. (Don't include 189 | the brackets!) The text should be enclosed in the appropriate 190 | comment syntax for the file format. We also recommend that a 191 | file or class name and description of purpose be included on the 192 | same "printed page" as the copyright notice for easier 193 | identification within third-party archives. 194 | 195 | Copyright 2018 CAPSiDE 196 | 197 | Licensed under the Apache License, Version 2.0 (the "License"); 198 | you may not use this file except in compliance with the License. 199 | You may obtain a copy of the License at 200 | 201 | http://www.apache.org/licenses/LICENSE-2.0 202 | 203 | Unless required by applicable law or agreed to in writing, software 204 | distributed under the License is distributed on an "AS IS" BASIS, 205 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 206 | See the License for the specific language governing permissions and 207 | limitations under the License. 208 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS Network Graph 2 | 3 | This is a small program for getting a hold of the state of your AWS network. It maps out 4 | a VPC region of your choice 5 | 6 | ## Example 7 | 8 | ![Graph Example](https://raw.githubusercontent.com/pplu/aws-map/master/examples/graph1.png) 9 | 10 | ## Installation 11 | 12 | On a recent Ubuntu system these packages are needed 13 | 14 | ``` 15 | apt-get install -y graphviz-dev libxml2-dev libssl-dev carton 16 | git clone https://github.com/pplu/aws-map.git 17 | cd aws-map 18 | carton install 19 | ``` 20 | 21 | ## Generating images 22 | 23 | You can scan your infrastructure with two utilities: 24 | 25 | ``` 26 | carton exec perl -I lib bin/map_network_sgs eu-west-1 27 | ``` 28 | 29 | This will generate three files: `graph.svg`, `graph.dot` and `graph.png`. These 30 | all have the same contents in different formats SVG, DOT (for graphviz) and PNG 31 | 32 | Optionally you can pass a second parameter with the prefix for the the images to 33 | be generated. Note that the three extensions will be added to the prefix 34 | 35 | ## Self-Hosted web server 36 | 37 | ``` 38 | carton exec perl -I lib bin/map-webserver eu-west-1 39 | ``` 40 | 41 | This will prompt you to visit `http://localhost:3000` where there is a small web application 42 | that has a viewer with zooming and panning. This is very convenient to navigate the map 43 | (specially big ones) 44 | 45 | ## Understanding the graph 46 | 47 | The generated graph attempts to show you your AWS region from a networking perspetive. It shows you what can talk to what, at an IP level. 48 | 49 | The graphs' nodes are "things" that can talk IP (Network hosts, Instances, etc.) 50 | 51 | ![Network Icon](https://github.com/pplu/aws-map/raw/master/icons/network.png) Network Hosts and Network Ranges. 52 | 53 | ![Internet Icon](https://github.com/pplu/aws-map/raw/master/icons/internet.png) We have a special icon for 0.0.0.0/0, tagging it as "The Internet" 54 | 55 | ![Instance Icon](https://github.com/pplu/aws-map/raw/master/icons/i.png)...![RDS Icon](https://github.com/pplu/aws-map/raw/master/icons/rds.png) Instances, RDSs, ELBs... (AWS objects) are represented with their respetive icons. If there is no icon the object is just a box. 56 | 57 | ![Security Group Icon](https://github.com/pplu/aws-map/raw/master/icons/security_group.png) are Security Groups with nothing in them. You may want to evaluate deleting them. 58 | 59 | Instances in an autoscaling group will be surrounded in a dotted box with "autoscaling arrows" to left and right. 60 | 61 | Arrows tell you in what direction IP connections (TCP, UDP, ICMP, etc) can flow (what can talk to what). Only incoming connections are graphed (Outbound rules aren't scanned yet). When a port range is not labeled, it means that the ports are TCP (i.e.: "25" means TCP port 25. "25-27" means TCP ports 25 to 27). If the ports are UDP, they are indicated: "25-27 UDP"). 62 | 63 | With a quick look at the example graph we can see the following: 64 | 65 | ![Graph Example](https://raw.githubusercontent.com/pplu/aws-map/master/examples/graph1.png) 66 | 67 | Things in 1.1.1.1/32 can talk to the instances via HTTP and SSH. 68 | 69 | The ELB is open to the Internet via HTTPS. It talks to instances via HTTP. 70 | 71 | The instances talk to an RDS on port 3306 72 | 73 | ## Known limitations 74 | 75 | This tool only evaluates incoming Security Group rules. That means that Subnet ACLs, Routing tables, etc. are not taken into account to calculate if a host can actually talk to another. 76 | 77 | When you graph a big account, it can take a while. Be patient. Also take into account that the graph can be hard to look at. 78 | 79 | ## Contributing 80 | 81 | Contributions are more than welcome. Take a look at the Perl Graphviz module to control the graph better: https://metacpan.org/pod/GraphViz2 82 | 83 | The source code is located here: https://github.com/pplu/aws-map 84 | 85 | Issuses can be opened here: https://github.com/pplu/aws-map/issues 86 | 87 | ## Author 88 | 89 | Jose Luis Martinez Torres (joseluis.martinez@capside.com) 90 | 91 | ## Copyright 92 | 93 | Copyright (c) 2017 by CAPSiDE 94 | 95 | This program is free software; you can redistribute 96 | it and/or modify it under the same terms as Perl itself. 97 | 98 | The full text of the license can be found in the 99 | LICENSE file included with this module. 100 | 101 | Icons come from [AWS Simple Icons collection](https://aws.amazon.com/es/architecture/icons/) and are (c) AWS 102 | 103 | -------------------------------------------------------------------------------- /bin/map-webserver: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | use AWS::Network::SecurityGroupMap; 4 | use Mojolicious::Lite; 5 | use Image::Size; 6 | 7 | my $region = shift @ARGV or die "Please specify region"; 8 | @ARGV = ('daemon'); 9 | 10 | my $map = AWS::Network::SecurityGroupMap->new( 11 | region => $region, 12 | ); 13 | 14 | $map->scan; 15 | $map->draw; 16 | $map->graphviz->run(format => 'png'); 17 | my $image = $map->graphviz->dot_output; 18 | my ($image_width, $image_height) = imgsize(\$image); 19 | 20 | get '/graph.png' => sub { 21 | my $c = shift; 22 | $c->res->headers->content_type('image/png'); 23 | $c->render(data => $image); 24 | }; 25 | 26 | get '/' => sub { 27 | my $c = shift; 28 | $c->stash(image_width => $image_width); 29 | $c->stash(image_height => $image_height); 30 | $c->render(template => 'index'); 31 | }; 32 | 33 | app->start; 34 | 35 | __DATA__ 36 | @@ index.html.ep 37 | 38 | 39 | 43 | 51 | 52 | 53 |
54 | 58 | 59 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /bin/map_network_sgs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | use v5.10; 4 | use AWS::Network::SecurityGroupMap; 5 | 6 | my $region = $ARGV[0] or die "Usage: $0 region [name]"; 7 | my $name = $ARGV[1] || 'graph'; 8 | 9 | my $map = AWS::Network::SecurityGroupMap->new( 10 | region => $region, 11 | ); 12 | 13 | say "Scanning AWS $region"; 14 | $map->scan; 15 | say "Generating graph"; 16 | $map->draw; 17 | say "Generating $name.dot"; 18 | $map->graphviz->run(format => 'dot', output_file => "$name.dot"); 19 | say "Generating $name.svg"; 20 | $map->graphviz->run(format => 'svg', output_file => "$name.svg"); 21 | say "Generating $name.png"; 22 | $map->graphviz->run(format => 'png', output_file => "$name.png"); 23 | say "Done"; 24 | -------------------------------------------------------------------------------- /cpanfile: -------------------------------------------------------------------------------- 1 | requires 'Paws'; 2 | requires 'GraphViz2'; 3 | recommends 'Mojo::Lite'; 4 | recommends 'Image::Size'; 5 | -------------------------------------------------------------------------------- /examples/graph1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pplu/aws-map/e0e046d65b92de5ceaf6f5a490bf7c108e4a8ad3/examples/graph1.png -------------------------------------------------------------------------------- /icons/alb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pplu/aws-map/e0e046d65b92de5ceaf6f5a490bf7c108e4a8ad3/icons/alb.png -------------------------------------------------------------------------------- /icons/asg-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pplu/aws-map/e0e046d65b92de5ceaf6f5a490bf7c108e4a8ad3/icons/asg-left.png -------------------------------------------------------------------------------- /icons/asg-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pplu/aws-map/e0e046d65b92de5ceaf6f5a490bf7c108e4a8ad3/icons/asg-right.png -------------------------------------------------------------------------------- /icons/elb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pplu/aws-map/e0e046d65b92de5ceaf6f5a490bf7c108e4a8ad3/icons/elb.png -------------------------------------------------------------------------------- /icons/i.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pplu/aws-map/e0e046d65b92de5ceaf6f5a490bf7c108e4a8ad3/icons/i.png -------------------------------------------------------------------------------- /icons/internet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pplu/aws-map/e0e046d65b92de5ceaf6f5a490bf7c108e4a8ad3/icons/internet.png -------------------------------------------------------------------------------- /icons/memcached.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pplu/aws-map/e0e046d65b92de5ceaf6f5a490bf7c108e4a8ad3/icons/memcached.png -------------------------------------------------------------------------------- /icons/network.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pplu/aws-map/e0e046d65b92de5ceaf6f5a490bf7c108e4a8ad3/icons/network.png -------------------------------------------------------------------------------- /icons/rds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pplu/aws-map/e0e046d65b92de5ceaf6f5a490bf7c108e4a8ad3/icons/rds.png -------------------------------------------------------------------------------- /icons/redis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pplu/aws-map/e0e046d65b92de5ceaf6f5a490bf7c108e4a8ad3/icons/redis.png -------------------------------------------------------------------------------- /icons/redshift.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pplu/aws-map/e0e046d65b92de5ceaf6f5a490bf7c108e4a8ad3/icons/redshift.png -------------------------------------------------------------------------------- /icons/security_group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pplu/aws-map/e0e046d65b92de5ceaf6f5a490bf7c108e4a8ad3/icons/security_group.png -------------------------------------------------------------------------------- /lib/AWS/Network/SecurityGroupMap.pm: -------------------------------------------------------------------------------- 1 | package AWS::Map::Object { 2 | use Moose; 3 | has type => (is => 'ro', isa => 'Str', required => 1); 4 | has name => (is => 'ro', isa => 'Str', required => 1); 5 | has label => (is => 'ro', isa => 'Str'); 6 | has belongs_to => (is => 'ro', isa => 'Str'); 7 | 8 | has icon => (is => 'ro', isa => 'Maybe[Str]', lazy => 1, default => sub { 9 | my $self = shift; 10 | return undef if (not defined $self->type); 11 | my $file = sprintf 'icons/%s.png', $self->type; 12 | return $file if (-e $file); 13 | warn "Can't find file $file"; 14 | return undef; 15 | }); 16 | } 17 | package AWS::Map::SG { 18 | use Moose; 19 | has name => (is => 'ro', isa => 'Str', required => 1); 20 | has label => (is => 'ro', isa => 'Str', required => 1); 21 | has listens_to => (is => 'ro', isa => 'HashRef', default => sub { {} }); 22 | 23 | sub set_listens_to { 24 | my ($self, $o2, $port) = @_; 25 | 26 | $self->listens_to->{ $o2 } = [] if (not defined $self->listens_to->{ $o2 }); 27 | push @{ $self->listens_to->{ $o2 } }, $port; 28 | } 29 | } 30 | package AWS::Network::SecurityGroupMap { 31 | use v5.10; 32 | use feature 'postderef'; 33 | use Moose; 34 | use GraphViz2; 35 | use Paws; 36 | use AWS::Map::Object; 37 | no warnings 'experimental::postderef'; 38 | 39 | has graphviz => ( 40 | is => 'ro', 41 | lazy => 1, 42 | default => sub { 43 | my $self = shift; 44 | GraphViz2->new( 45 | global => { directed => 1, ranksep => 5 }, 46 | graph => { 47 | label => $self->title, 48 | }, 49 | ); 50 | } 51 | ); 52 | 53 | has aws => (is => 'ro', isa => 'Paws', lazy => 1, default => sub { 54 | my $self = shift; 55 | Paws->new(config => { 56 | region => $self->region, 57 | }); 58 | }); 59 | 60 | has region => (is => 'ro', isa => 'Str', required => 1); 61 | has title => (is => 'ro', isa => 'Str', lazy => 1, default => sub { 62 | my $self = shift; 63 | "Mapped by https://github.com/pplu/aws-map for region " . $self->region . " on " . scalar(localtime); 64 | }); 65 | 66 | has _objects => ( 67 | is => 'ro', 68 | isa => 'HashRef[AWS::Map::Object]', 69 | default => sub { {} }, 70 | traits => [ 'Hash' ], 71 | handles => { 72 | objects => 'values', 73 | } 74 | ); 75 | 76 | sub add_object { 77 | my ($self, %args) = @_; 78 | my $o = AWS::Map::Object->new(%args); 79 | $self->_objects->{ $o->name } = $o; 80 | } 81 | 82 | has _sg => ( 83 | is => 'ro', 84 | isa => 'HashRef[AWS::Map::SG]', 85 | default => sub { {} } 86 | ); 87 | 88 | sub get_sg { 89 | my ($self, $sg) = @_; 90 | return $self->_sg->{ $sg }; 91 | } 92 | 93 | # holds what objects an SG contains 94 | has _contains => ( 95 | is => 'ro', 96 | isa => 'HashRef', 97 | default => sub { {} } 98 | ); 99 | 100 | sub sg_holds { 101 | my ($self, $sg, $object) = @_; 102 | $self->_contains->{ $sg }->{ $object } = 1; 103 | } 104 | 105 | sub get_objects_in_sg { 106 | my ($self, $sg) = @_; 107 | keys %{ $self->_contains->{ $sg } }; 108 | } 109 | 110 | sub get_who_listens { 111 | my $self = shift; 112 | return keys %{ $self->_sg }; 113 | } 114 | 115 | sub get_listens_to { 116 | my ($self, $o1) = @_; 117 | return keys %{ $self->_sg->{ $o1 }->listens_to }; 118 | } 119 | 120 | sub get_listens_on_ports { 121 | my ($self, $o1, $o2) = @_; 122 | return @{ $self->_sg->{ $o1 }->listens_to->{ $o2 } }; 123 | } 124 | 125 | sub register_sg { 126 | my ($self, %params) = @_; 127 | die "Can't register without a name" if (not defined $params{ name }); 128 | return $self->_sg->{ $params{ name } } = AWS::Map::SG->new(%params); 129 | } 130 | 131 | 132 | sub _scan_elbs { 133 | my $self = shift; 134 | 135 | $self->aws->service('ELB')->DescribeAllLoadBalancers(sub { 136 | my $elb = shift; 137 | 138 | $self->add_object(name => $elb->LoadBalancerName, type => 'elb'); 139 | 140 | foreach my $sg ($elb->SecurityGroups->@*) { 141 | $self->sg_holds($sg, $elb->LoadBalancerName); 142 | } 143 | }); 144 | } 145 | 146 | sub _scan_elbv2s { 147 | my $self = shift; 148 | 149 | $self->aws->service('ELBv2')->DescribeAllLoadBalancers(sub { 150 | my $elb = shift; 151 | 152 | $self->add_object(name => $elb->LoadBalancerName, type => 'alb'); 153 | 154 | return if (not defined $elb->SecurityGroups); 155 | 156 | foreach my $sg ($elb->SecurityGroups->@*) { 157 | $self->sg_holds($sg, $elb->LoadBalancerName); 158 | } 159 | }); 160 | } 161 | 162 | sub _scan_redshift { 163 | my $self = shift; 164 | 165 | $self->aws->service('RedShift')->DescribeAllClusters(sub { 166 | my $cluster = shift; 167 | $self->add_object(name => $cluster->ClusterIdentifier, type => 'redshift'); 168 | 169 | foreach my $sg ($cluster->VpcSecurityGroups->@*) { 170 | $self->sg_holds($sg->VpcSecurityGroupId, $cluster->ClusterIdentifier); 171 | } 172 | }); 173 | } 174 | 175 | sub _scan_rds { 176 | my $self = shift; 177 | 178 | $self->aws->service('RDS')->DescribeAllDBInstances(sub { 179 | my $instance = shift; 180 | $self->add_object(name => $instance->DBInstanceIdentifier, type => 'rds'); 181 | 182 | foreach my $sg ($instance->VpcSecurityGroups->@*) { 183 | $self->sg_holds($sg->VpcSecurityGroupId, $instance->DBInstanceIdentifier); 184 | } 185 | }); 186 | 187 | #TODO: DescribeAllDBClusters doesn't have a paginator 188 | #$self->aws->service('RDS')->DescribeDBClusters 189 | } 190 | 191 | sub _scan_autoscalinggroups { 192 | my $self = shift; 193 | 194 | $self->aws->service('AutoScaling')->DescribeAllAutoScalingGroups(sub { 195 | my $asg = shift; 196 | 197 | $self->add_object( 198 | name => $asg->AutoScalingGroupName, 199 | type => 'asg', 200 | ); 201 | }); 202 | } 203 | 204 | sub _scan_instances { 205 | my $self = shift; 206 | 207 | $self->aws->service('EC2')->DescribeAllInstances(sub { 208 | my $rsv = shift; 209 | foreach my $instance ($rsv->Instances->@*) { 210 | # Derive information from tags 211 | # Get the value of a tag named 'Name' from the list of tag objects 212 | my ($tag) = map { $_->Value } grep { $_->Key eq 'Name' } $instance->Tags->@*; 213 | my ($asg_name) = map { $_->Value } grep { $_->Key eq 'aws:autoscaling:groupName' } $instance->Tags->@*; 214 | 215 | $self->add_object( 216 | name => $instance->InstanceId, 217 | type => 'i', 218 | (defined $tag)?(label => $tag):(), 219 | (defined $asg_name) ? (belongs_to => $asg_name) : (), 220 | ); 221 | 222 | foreach my $sg ($instance->SecurityGroups->@*) { 223 | $self->sg_holds($sg->GroupId, $instance->InstanceId); 224 | } 225 | } 226 | }); 227 | } 228 | 229 | sub _scan_elasticache { 230 | my $self = shift; 231 | 232 | $self->aws->service('ElastiCache')->DescribeAllCacheClusters(sub { 233 | my $cluster = shift; 234 | my $engine = $cluster->Engine; # either memcached or redis 235 | $self->add_object(name => $cluster->CacheClusterId, type => $engine); 236 | 237 | foreach my $sg ($cluster->SecurityGroups->@*) { 238 | $self->sg_holds($sg->SecurityGroupId, $cluster->CacheClusterId); 239 | } 240 | }); 241 | 242 | } 243 | 244 | sub _scan_securitygroups { 245 | my $self = shift; 246 | 247 | my $sgs = $self->aws->service('EC2')->DescribeSecurityGroups; 248 | foreach my $sg ($sgs->SecurityGroups->@*) { 249 | my $model = $self->register_sg(name => $sg->GroupId, label => $sg->Description); 250 | 251 | foreach my $ip_perm ($sg->IpPermissions->@*){ 252 | my $port; 253 | if ($ip_perm->IpProtocol eq 'icmp') { 254 | $port = 'ICMP'; 255 | } elsif ($ip_perm->IpProtocol eq '-1') { 256 | $port = 'All IP traffic'; 257 | } else { 258 | if ($ip_perm->FromPort == $ip_perm->ToPort) { 259 | $port = $ip_perm->FromPort; 260 | } else { 261 | $port = $ip_perm->FromPort . '-' . $ip_perm->ToPort; 262 | } 263 | $port .= ' UDP' if ($ip_perm->IpProtocol eq 'udp'); 264 | } 265 | 266 | foreach my $ip_r ($ip_perm->IpRanges->@*) { 267 | $model->set_listens_to($ip_r->CidrIp, $port); 268 | } 269 | foreach my $ip_r ($ip_perm->Ipv6Ranges->@*) { 270 | $model->set_listens_to($ip_r->CidrIpv6, $port); 271 | } 272 | foreach my $ip_p ($ip_perm->PrefixListIds->@*) { 273 | die "Don't know how to handle PrefixLists yet"; 274 | } 275 | foreach my $ip_p ($ip_perm->UserIdGroupPairs->@*) { 276 | $model->set_listens_to($ip_p->GroupId, $port); 277 | } 278 | } 279 | } 280 | } 281 | 282 | sub scan { 283 | my $self = shift; 284 | 285 | $self->_scan_instances; 286 | $self->_scan_autoscalinggroups; 287 | $self->_scan_elbs; 288 | $self->_scan_elbv2s; 289 | $self->_scan_rds; 290 | $self->_scan_redshift; 291 | $self->_scan_elasticache; 292 | #$self->_scan_efs; 293 | #$self->_scan_dax; 294 | #$self->_scan_emr; 295 | 296 | $self->_scan_securitygroups; 297 | } 298 | 299 | sub ip_to_object { 300 | my ($self, $ip) = @_; 301 | 302 | my $label = ($ip eq '0.0.0.0/0') ? 'The Internet' : $ip; 303 | my $type = ($ip eq '0.0.0.0/0') ? 'internet' : 'network'; 304 | 305 | return AWS::Map::Object->new( 306 | type => $type, 307 | name => $ip, 308 | label => $label 309 | ); 310 | } 311 | 312 | sub draw { 313 | my ($self) = @_; 314 | 315 | my %font_config = (fontname => 'Lucida', fontsize => 10); 316 | $self->graphviz->default_node (%font_config, shape => 'none'); 317 | $self->graphviz->default_edge (%font_config); 318 | $self->graphviz->default_graph(%font_config); 319 | 320 | my $groups = {}; 321 | foreach my $object ($self->objects) { 322 | next if ($object->type eq 'asg'); 323 | 324 | my $group = $object->belongs_to; 325 | $group = 'default' if (not defined $group); 326 | 327 | $groups->{ $group }->{ $object->name } = $object; 328 | } 329 | 330 | foreach my $group_name (keys $groups->%*) { 331 | if ($group_name ne 'default') { 332 | $self->graphviz->push_subgraph( 333 | name => "cluster_$group_name", 334 | graph => { label => $group_name, style => 'dotted' } 335 | ); 336 | 337 | $self->graphviz->add_node(name => "$group_name-scale-r", label => '', image => 'icons/asg-right.png'); 338 | } 339 | 340 | foreach my $object (keys $groups->{ $group_name }->%*) { 341 | my $object = $groups->{ $group_name }->{ $object }; 342 | 343 | my %extra = (); 344 | #$extra{ labelloc } = 't'; 345 | $extra{ label } = $object->name; 346 | $extra{ label } .= ' ' . $object->label if (defined $object->label); 347 | 348 | if (defined $object->icon) { 349 | $extra{ image } = $object->icon 350 | } else { 351 | $extra{ shape } = 'box'; 352 | } 353 | 354 | $self->graphviz->add_node(name => $object->name, %extra); 355 | } 356 | 357 | if ($group_name ne 'default') { 358 | $self->graphviz->add_node(name => "$group_name-scale-l", label => '', image => 'icons/asg-left.png'); 359 | 360 | $self->graphviz->pop_subgraph; 361 | } 362 | } 363 | 364 | foreach my $listener ($self->get_who_listens) { 365 | # listeners are names of security groups. There can be lots of things in an SG 366 | my @things_in_sg = $self->get_objects_in_sg($listener); 367 | if (not @things_in_sg) { 368 | my $sg = $self->get_sg($listener); 369 | if (defined $sg) { 370 | my $label = $sg->name . ' ' . $sg->label if (defined $sg->label); 371 | $self->graphviz->add_node(name => $sg->name, label => $sg->label, image => 'icons/security_group.png'); 372 | @things_in_sg = ($listener); 373 | } else { 374 | my $ip = $self->ip_to_object($listener); 375 | $self->graphviz->add_node(name => $ip->name, label => $ip->label, image => $ip->icon); 376 | @things_in_sg = ($listener); 377 | } 378 | } 379 | 380 | foreach my $thing_in_sg (@things_in_sg) { 381 | foreach my $listened_to ($self->get_listens_to($listener)){ 382 | my @things_in_sg2 = $self->get_objects_in_sg($listened_to); 383 | if (not @things_in_sg2) { 384 | my $sg = $self->get_sg($listened_to); 385 | if (defined $sg) { 386 | my $label = $sg->name . ' ' . $sg->label if (defined $sg->label); 387 | $self->graphviz->add_node(name => $sg->name, label => $sg->label, image => 'icons/security_group.png'); 388 | @things_in_sg2 = ($listened_to); 389 | } else { 390 | my $ip = $self->ip_to_object($listened_to); 391 | $self->graphviz->add_node(name => $ip->name, label => $ip->label, image => $ip->icon); 392 | @things_in_sg2 = ($listened_to); 393 | } 394 | } 395 | 396 | 397 | foreach my $thing_listened_to (@things_in_sg2){ 398 | my $label = join ', ', $self->get_listens_on_ports($listener, $listened_to); 399 | $self->graphviz->add_edge( 400 | from => $thing_listened_to, 401 | to => $thing_in_sg, 402 | label => $label, 403 | ); 404 | } 405 | } 406 | } 407 | } 408 | } 409 | } 410 | 1; 411 | -------------------------------------------------------------------------------- /t/test_1.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | use strict; 4 | use warnings; 5 | use AWS::Network::SecurityGroupMap; 6 | 7 | my $map = AWS::Network::SecurityGroupMap->new( 8 | region => 'x', 9 | ); 10 | 11 | my $sg1 = $map->register_sg(name => 'sg-1', label => 'ELBToWorld'); 12 | $sg1->set_listens_to('0.0.0.0/0', 443); 13 | my $sg2 = $map->register_sg(name => 'sg-2', label => 'InstancesToELB'); 14 | $sg2->set_listens_to('sg-1', 80); 15 | $sg2->set_listens_to('1.1.1.1/32', 22); 16 | $sg2->set_listens_to('1.1.1.1/32', 80); 17 | my $sg3 = $map->register_sg(name => 'sg-3', label => 'RDS'); 18 | $sg3->set_listens_to('sg-2', 3306); 19 | 20 | 21 | $map->add_object(name => 'elb-XXX', type => 'elb'); 22 | $map->sg_holds('sg-1', 'elb-XXX'); 23 | 24 | foreach my $i ('i-11111111','i-11111112','i-11111113','i-11111114') { 25 | $map->add_object(name => $i, type => 'i'); 26 | $map->sg_holds('sg-2', $i); 27 | } 28 | 29 | $map->add_object(name => 'RDSXYZ', type => 'rds'); 30 | $map->sg_holds('sg-3', 'RDSXYZ'); 31 | 32 | $map->draw; 33 | --------------------------------------------------------------------------------