aws_recon 0.2.1 → 0.2.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a8cd90cd73ab80c766882e88a2bbd5f03c106a7365805b7e50847997decd98de
4
- data.tar.gz: 1a605e550317fdbf2295f72c0df9df6bdb02b1cb70e940d83db3c779d165d7d3
3
+ metadata.gz: 2707134a4d4be23f0f1cd3320fda7e75463aae49113b2f4b802257aa02b0de52
4
+ data.tar.gz: 85452e37a3657d315fae2b67852bc521786a95fc386db53f4a3212801e140ee4
5
5
  SHA512:
6
- metadata.gz: f3f8600403036263543e0be3d9fee779d40a9cedbed38a982f8144e645c1184d0e4c135691d87d0e7809033c55e371dab4eaaba8ff80b6ba27baab7663d75ec0
7
- data.tar.gz: 7e4af190e9451570c40e90fdd222899bc11c072da15f496d7739cf057da5ee04615d6a6b0cfda375308fda88a9dd91796bb0ab8e31e6d3289f7cf4daf7286e3c
6
+ metadata.gz: 5e1a919375c34b51a72a3bb470ce178b39c178e8e3a569fb47f618c8ac122512e14c2564e42bd72aab3e5a550041f8e9490c3f3d6245ac2d3c933e77a9053d18
7
+ data.tar.gz: e93eaad39853ffdd5c934572b2d9fb2add88efc0150bc48cbc3cf7cef542c25a98a2ebd1c92e6b64404dde122d86d8d2d38638f6bd5fde0e11974c2c52e5cdc1
@@ -0,0 +1,17 @@
1
+ # Number of days of inactivity before an issue becomes stale
2
+ daysUntilStale: 30
3
+ # Number of days of inactivity before a stale issue is closed
4
+ daysUntilClose: 5
5
+ # Issues with these labels will never be considered stale
6
+ exemptLabels:
7
+ - pinned
8
+ - security
9
+ # Label to use when marking an issue as stale
10
+ staleLabel: wontfix
11
+ # Comment to post when marking an issue as stale. Set to `false` to disable
12
+ markComment: >
13
+ This issue has been automatically marked as stale because it has not had
14
+ recent activity. It will be closed if no further activity occurs. Thank you
15
+ for your contributions.
16
+ # Comment to post when closing a stale issue. Set to `false` to disable
17
+ closeComment: false
data/.gitignore CHANGED
@@ -1,5 +1,6 @@
1
1
  .DS_Store
2
2
  *.json
3
+ Gemfile.lock
3
4
  .rvmrc
4
5
  .ruby-gemset
5
6
  .ruby-version
@@ -7,10 +7,10 @@ require 'aws_recon/version'
7
7
  Gem::Specification.new do |spec|
8
8
  spec.name = 'aws_recon'
9
9
  spec.version = AwsRecon::VERSION
10
- spec.authors = ['Josh Larsen']
10
+ spec.authors = ['Josh Larsen', 'Darkbit']
11
11
  spec.required_ruby_version = '>= 2.5.0'
12
- spec.summary = 'A multi-threaded AWS inventory collection tool.'
13
- spec.description = spec.summary
12
+ spec.summary = 'A multi-threaded AWS inventory collection cli tool.'
13
+ spec.description = 'AWS Recon is a command line tool to collect resources from an Amazon Web Services (AWS) account. The tool outputs JSON suitable for processing with other tools.'
14
14
  spec.homepage = 'https://github.com/darkbitio/aws-recon'
15
15
  spec.license = 'MIT'
16
16
 
@@ -28,7 +28,7 @@ Gem::Specification.new do |spec|
28
28
 
29
29
  spec.add_development_dependency 'bundler', '~> 1.17'
30
30
  spec.add_development_dependency 'gem-release', '~> 2.1'
31
- spec.add_development_dependency 'rake', '~> 10.0'
31
+ spec.add_development_dependency 'rake', '>= 12.3.3'
32
32
  spec.add_development_dependency 'minitest', '~> 5.0'
33
33
  spec.add_development_dependency 'solargraph', '~> 0.39.11'
34
34
  spec.add_development_dependency 'rubocop', '~> 0.87.1'
@@ -1,5 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ # for local testing
4
+ $LOAD_PATH.unshift(File.expand_path(File.join('..', '..', 'lib'), __FILE__))
5
+
3
6
  require 'aws_recon'
4
7
 
5
8
  AwsRecon::CLI.new.start(ARGV)
@@ -8,12 +8,11 @@ require 'ostruct'
8
8
  require 'optparse'
9
9
  require 'yaml'
10
10
  require 'csv'
11
- require 'pry'
12
11
  require 'aws-sdk'
13
12
  require 'aws_recon/options.rb'
14
13
  require 'aws_recon/lib/mapper.rb'
15
14
  require 'aws_recon/lib/formatter.rb'
16
- require 'aws_recon/collectors/collectors.rb'
15
+ require 'aws_recon/collectors.rb'
17
16
 
18
17
  require 'aws_recon/version'
19
18
  require 'aws_recon/aws_recon'
@@ -4,37 +4,39 @@ SERVICES_CONFIG_FILE = File.join(File.dirname(__FILE__), 'services.yaml').freeze
4
4
 
5
5
  module AwsRecon
6
6
  class CLI
7
- # parse options
8
- @options = Parser.parse ARGV.length < 1 ? %w[-h] : ARGV
7
+ def initialize
8
+ # parse options
9
+ @options = Parser.parse ARGV.length < 1 ? %w[-h] : ARGV
9
10
 
10
- # timing
11
- starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
11
+ # timing
12
+ @starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
12
13
 
13
- # AWS account id
14
- @account_id = Aws::STS::Client.new.get_caller_identity.account
14
+ # AWS account id
15
+ @account_id = Aws::STS::Client.new.get_caller_identity.account
15
16
 
16
- # AWS services
17
- aws_services = YAML.load(File.read(SERVICES_CONFIG_FILE), symbolize_names: true)
17
+ # AWS services
18
+ @aws_services = YAML.load(File.read(SERVICES_CONFIG_FILE), symbolize_names: true)
18
19
 
19
- # User config services
20
- if @options.config_file
21
- user_config = YAML.load(File.read(@options.config_file), symbolize_names: true)
20
+ # User config services
21
+ if @options.config_file
22
+ user_config = YAML.load(File.read(@options.config_file), symbolize_names: true)
22
23
 
23
- services = user_config[:services]
24
- regions = user_config[:regions]
25
- else
26
- regions = @options.regions
27
- services = @options.services
28
- end
24
+ @services = user_config[:services]
25
+ @regions = user_config[:regions]
26
+ else
27
+ @regions = @options.regions
28
+ @services = @options.services
29
+ end
29
30
 
30
- # collection
31
- @resources = []
31
+ # collection
32
+ @resources = []
32
33
 
33
- # formatter
34
- @formatter = Formatter.new
34
+ # formatter
35
+ @formatter = Formatter.new
35
36
 
36
- unless @options.stream_output
37
- puts "\nStarting collection with #{@options.threads} threads...\n"
37
+ unless @options.stream_output
38
+ puts "\nStarting collection with #{@options.threads} threads...\n"
39
+ end
38
40
  end
39
41
 
40
42
  #
@@ -66,16 +68,16 @@ module AwsRecon
66
68
  #
67
69
  # main wrapper
68
70
  #
69
- def start
71
+ def start(_args)
70
72
  #
71
73
  # global services
72
74
  #
73
- aws_services.map { |x| OpenStruct.new(x) }.filter { |s| s.global }.each do |service|
75
+ @aws_services.map { |x| OpenStruct.new(x) }.filter { |s| s.global }.each do |service|
74
76
  # user included this service in the args
75
- next unless services.include?(service.name)
77
+ next unless @services.include?(service.name)
76
78
 
77
79
  # user did not exclude 'global'
78
- next unless regions.include?('global')
80
+ next unless @regions.include?('global')
79
81
 
80
82
  collect(service, 'global')
81
83
  end
@@ -83,28 +85,28 @@ module AwsRecon
83
85
  #
84
86
  # regional services
85
87
  #
86
- regions.filter { |x| x != 'global' }.each do |region|
87
- Parallel.map(aws_services.map { |x| OpenStruct.new(x) }.filter { |s| !s.global }.each, in_threads: @options.threads) do |service|
88
+ @regions.filter { |x| x != 'global' }.each do |region|
89
+ Parallel.map(@aws_services.map { |x| OpenStruct.new(x) }.filter { |s| !s.global }.each, in_threads: @options.threads) do |service|
88
90
  # some services aren't available in some regions
89
91
  skip_region = service&.excluded_regions&.include?(region)
90
92
 
91
93
  # user included this region in the args
92
- next unless regions.include?(region) && !skip_region
94
+ next unless @regions.include?(region) && !skip_region
93
95
 
94
96
  # user included this service in the args
95
- next unless services.include?(service.name) || services.include?(service.alias) # rubocop:disable Layout/LineLength
97
+ next unless @services.include?(service.name) || @services.include?(service.alias) # rubocop:disable Layout/LineLength
96
98
 
97
99
  collect(service, region)
98
100
  end
99
101
  end
100
102
  rescue Interrupt # ctrl-c
101
- elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - starting
103
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @starting
102
104
 
103
105
  puts "\nStopped early after \x1b[32m#{elapsed.to_i}\x1b[0m seconds.\n"
104
106
  ensure
105
107
  # write output file
106
108
  if @options.output_file
107
- elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - starting
109
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @starting
108
110
 
109
111
  puts "\nFinished in \x1b[32m#{elapsed.to_i}\x1b[0m seconds. Saving resources to \x1b[32m#{@options.output_file}\x1b[0m.\n\n"
110
112
 
@@ -0,0 +1,2 @@
1
+ # require all collectors
2
+ Dir[File.join(__dir__, 'collectors', '*.rb')].each { |file| require file }
@@ -48,6 +48,23 @@ class EC2 < Mapper
48
48
  struct.arn = instance.instance_id # no true ARN
49
49
  struct.reservation_id = reservation.reservation_id
50
50
 
51
+ # collect instance user_data
52
+ if @options.collect_user_data
53
+ user_data_raw = @client.describe_instance_attribute({
54
+ attribute: 'userData',
55
+ instance_id: instance.instance_id
56
+ }).user_data.to_h[:value]
57
+
58
+ # don't save non-string user_data
59
+ if user_data_raw
60
+ user_data = Base64.decode64(user_data_raw)
61
+
62
+ if user_data.force_encoding('UTF-8').ascii_only?
63
+ struct.user_data = user_data
64
+ end
65
+ end
66
+ end
67
+
51
68
  resources.push(struct.to_h)
52
69
  end
53
70
  end
@@ -0,0 +1,22 @@
1
+ class ElastiCache < Mapper
2
+ def collect
3
+ resources = []
4
+
5
+ #
6
+ # describe_cache_clusters
7
+ #
8
+ @client.describe_cache_clusters.each_with_index do |response, page|
9
+ log(response.context.operation_name, page)
10
+
11
+ response.cache_clusters.each do |cluster|
12
+ struct = OpenStruct.new(cluster.to_h)
13
+ struct.type = 'cluster'
14
+ struct.arn = cluster.arn
15
+
16
+ resources.push(struct.to_h)
17
+ end
18
+ end
19
+
20
+ resources
21
+ end
22
+ end
@@ -1,6 +1,5 @@
1
1
  class Lambda < Mapper
2
2
  def collect
3
- service = self.class.to_s.downcase
4
3
  resources = []
5
4
 
6
5
  #
@@ -15,8 +15,6 @@ class S3 < Mapper
15
15
  log(response.context.operation_name, page)
16
16
 
17
17
  Parallel.map(response.buckets.each, in_threads: @options.threads) do |bucket|
18
- # use shared client instance
19
- client = @client
20
18
  @thread = Parallel.worker_number
21
19
  log(response.context.operation_name, bucket.name)
22
20
 
@@ -27,10 +25,14 @@ class S3 < Mapper
27
25
  # check bucket region constraint
28
26
  location = @client.get_bucket_location({ bucket: bucket.name }).location_constraint
29
27
 
30
- # reset client if needed
31
- unless location.empty?
32
- client = Aws::S3::Client.new({ region: location })
33
- end
28
+ # if you use a region other than the us-east-1 endpoint
29
+ # to create a bucket, you must set the location_constraint
30
+ # bucket parameter to the same region. (https://docs.aws.amazon.com/general/latest/gr/s3.html)
31
+ client = if location.empty?
32
+ @client
33
+ else
34
+ Aws::S3::Client.new({ region: location })
35
+ end
34
36
 
35
37
  operations = [
36
38
  { func: 'get_bucket_acl', key: 'acl', field: nil },
@@ -15,6 +15,13 @@
15
15
  # to add 5 seconds delay on each retry for a total max of 55 seconds.
16
16
  #
17
17
  class Mapper
18
+ # Services that use us-east-1 endpoint only:
19
+ # Organizations
20
+ # Route53Domains
21
+ # Shield
22
+ # S3 (unless the bucket was created in another region)
23
+ SINGLE_REGION_SERVICES = %w[route53domains s3 shield support organizations].freeze
24
+
18
25
  def initialize(service, region, options)
19
26
  @service = service
20
27
  @region = region
@@ -39,8 +46,8 @@ class Mapper
39
46
  # regional service
40
47
  client_options.merge!({ region: region }) unless region == 'global'
41
48
 
42
- # organizations only uses us-east-1 in non cn/gov regions
43
- client_options.merge!({ region: 'us-east-1' }) if service.downcase == 'organizations' # rubocop:disable Layout/LineLength
49
+ # single region services
50
+ client_options.merge!({ region: 'us-east-1' }) if SINGLE_REGION_SERVICES.include?(service.downcase) # rubocop:disable Layout/LineLength
44
51
 
45
52
  # debug with wire trace
46
53
  client_options.merge!({ http_wire_trace: true }) if @options.debug
@@ -2,7 +2,7 @@
2
2
 
3
3
  class Parser
4
4
  DEFAULT_CONFIG_FILE = nil
5
- DEFAULT_OUTPUT_FILE = File.join(File.dirname(__FILE__), '../output.json').freeze
5
+ DEFAULT_OUTPUT_FILE = File.expand_path(File.join(Dir.pwd, 'output.json')).freeze
6
6
  SERVICES_CONFIG_FILE = File.join(File.dirname(__FILE__), 'services.yaml').freeze
7
7
  DEFAULT_FORMAT = 'aws'
8
8
  DEFAULT_THREADS = 8
@@ -15,6 +15,7 @@ class Parser
15
15
  :output_file,
16
16
  :output_format,
17
17
  :threads,
18
+ :collect_user_data,
18
19
  :skip_slow,
19
20
  :stream_output,
20
21
  :verbose,
@@ -43,11 +44,12 @@ class Parser
43
44
  false,
44
45
  false,
45
46
  false,
47
+ false,
46
48
  false
47
49
  )
48
50
 
49
51
  opt_parser = OptionParser.new do |opts|
50
- opts.banner = "\n\x1b[32mAWS Recon\x1b[0m - AWS Inventory Collector\n\nUsage: aws_recon [options]"
52
+ opts.banner = "\n\x1b[32mAWS Recon\x1b[0m - AWS Inventory Collector (#{AwsRecon::VERSION})\n\nUsage: aws_recon [options]"
51
53
 
52
54
  # regions
53
55
  opts.on('-r', '--regions [REGIONS]', 'Regions to scan, separated by comma (default: all)') do |regions|
@@ -86,7 +88,7 @@ class Parser
86
88
 
87
89
  # output file
88
90
  opts.on('-o', '--output [OUTPUT]', 'Specify output file (default: output.json)') do |output|
89
- args.output_file = output
91
+ args.output_file = File.expand_path(File.join(Dir.pwd, output))
90
92
  end
91
93
 
92
94
  # output format
@@ -103,6 +105,11 @@ class Parser
103
105
  end
104
106
  end
105
107
 
108
+ # collect EC2 instance user data
109
+ opts.on('-u', '--user-data', 'Collect EC2 instance user data (default: false)') do
110
+ args.collect_user_data = true
111
+ end
112
+
106
113
  # skip slow operations
107
114
  opts.on('-z', '--skip-slow', 'Skip slow operations (default: false)') do
108
115
  args.skip_slow = true
@@ -33,6 +33,8 @@
33
33
  alias: elbv2
34
34
  excluded_regions:
35
35
  - ap-southeast-1
36
+ - name: ElastiCache
37
+ alias: elasticache
36
38
  - name: IAM
37
39
  global: true
38
40
  alias: iam
@@ -1,3 +1,3 @@
1
1
  module AwsRecon
2
- VERSION = "0.2.1"
2
+ VERSION = "0.2.6"
3
3
  end
data/readme.md CHANGED
@@ -1,3 +1,5 @@
1
+ [![Gem Version](https://badge.fury.io/rb/aws_recon.svg)](https://badge.fury.io/rb/aws_recon)
2
+
1
3
  # AWS Recon
2
4
 
3
5
  A multi-threaded AWS inventory collection tool.
@@ -24,17 +26,30 @@ Ruby 2.5.x or 2.6.x (developed and tested with 2.6.5)
24
26
 
25
27
  ### Installation
26
28
 
27
- Clone this repository, then install the required gems using `bundle`:
29
+ Install the gem:
28
30
 
29
31
  ```
30
- $ git clone git@github.com:darkbitio/aws-recon.git
31
- $ cd aws-recon
32
- $ bundle
32
+ $ gem install aws_recon
33
+ Fetching aws_recon-0.2.6.gem
34
+ Fetching aws-sdk-resources-3.76.0.gem
35
+ Fetching aws-sdk-3.0.1.gem
36
+ Fetching parallel-1.19.2.gem
33
37
  ...
34
- Using aws-sdk-core 3.103.0
38
+ Successfully installed aws-sdk-3.0.1
39
+ Successfully installed parallel-1.19.2
40
+ Successfully installed aws_recon-0.2.6
41
+ ```
42
+
43
+ Or add it to your Gemfile using `bundle`:
44
+
45
+ ```
46
+ $ bundle add aws_recon
47
+ Fetching gem metadata from https://rubygems.org/
48
+ Resolving dependencies...
35
49
  ...
36
- Bundle complete! 5 Gemfile dependencies, 259 gems now installed.
37
- Use `bundle info [gemname]` to see where a bundled gem is installed.
50
+ Using aws-sdk 3.0.1
51
+ Using parallel 1.19.2
52
+ Using aws_recon 0.2.2
38
53
  ```
39
54
 
40
55
  ## Usage
@@ -42,13 +57,13 @@ Use `bundle info [gemname]` to see where a bundled gem is installed.
42
57
  AWS Recon will leverage any AWS credentials currently available to the environment it runs in. If you are collecting from multiple accounts, you may want to leverage something like [aws-vault](https://github.com/99designs/aws-vault) to manage different credentials.
43
58
 
44
59
  ```
45
- $ aws-vault exec profile -- ./recon.rb
60
+ $ aws-vault exec profile -- aws_recon
46
61
  ```
47
62
 
48
63
  Plain environment variables will work fine too.
49
64
 
50
65
  ```
51
- $ AWS_PROFILE=<profile> ./recon.rb
66
+ $ AWS_PROFILE=<profile> aws_recon
52
67
  ```
53
68
 
54
69
  You may want to use the `-v` or `--verbose` flag initially to see status and activity while collection is running.
@@ -62,7 +77,7 @@ In verbose mode, the console output will show:
62
77
  The `t` prefix indicates which thread a particular request is running under. Region, service, and operation indicate which request operation is currently in progress and where.
63
78
 
64
79
  ```
65
- $ ./recon.rb -v
80
+ $ aws_recon -v
66
81
 
67
82
  t0.global.EC2.describe_account_attributes
68
83
  t2.global.S3.list_buckets
@@ -87,11 +102,11 @@ Finished in 46 seconds. Saving resources to output.json.
87
102
  #### Example command line options
88
103
 
89
104
  ```
90
- $ AWS_PROFILE=<profile> ./recon.rb -s S3,EC2 -r global,us-east-1,us-east-2
105
+ $ AWS_PROFILE=<profile> aws_recon -s S3,EC2 -r global,us-east-1,us-east-2
91
106
  ```
92
107
 
93
108
  ```
94
- $ AWS_PROFILE=<profile> ./recon.rb --services S3,EC2 --regions global,us-east-1,us-east-2
109
+ $ AWS_PROFILE=<profile> aws_recon --services S3,EC2 --regions global,us-east-1,us-east-2
95
110
  ```
96
111
 
97
112
  #### Errors
@@ -118,11 +133,11 @@ For regional services, a thread (up to the thread limit) is spawned for each ser
118
133
  Most users will want to limit collection to relevant services and regions. Running without any options will attempt to collect all resources from all 16 regular regions.
119
134
 
120
135
  ```
121
- $ ./recon.rb -h
136
+ $ aws_recon -h
122
137
 
123
- AWS Recon - AWS Inventory Collector
138
+ AWS Recon - AWS Inventory Collector (0.2.6)
124
139
 
125
- Usage: ./recon.rb [options]
140
+ Usage: aws_recon [options]
126
141
  -r, --regions [REGIONS] Regions to scan, separated by comma (default: all)
127
142
  -n, --not-regions [REGIONS] Regions to skip, separated by comma (default: none)
128
143
  -s, --services [SERVICES] Services to scan, separated by comma (default: all)
@@ -131,6 +146,7 @@ Usage: ./recon.rb [options]
131
146
  -o, --output [OUTPUT] Specify output file (default: output.json)
132
147
  -f, --format [FORMAT] Specify output format (default: aws)
133
148
  -t, --threads [THREADS] Specify max threads (default: 8, max: 128)
149
+ -u, --user-data Collect EC2 instance user data (default: false)
134
150
  -z, --skip-slow Skip slow operations (default: false)
135
151
  -j, --stream-output Stream JSON lines to stdout (default: false)
136
152
  -v, --verbose Output client progress and current operation
@@ -178,6 +194,7 @@ AWS Recon aims to collect all resources and metadata that are relevant in determ
178
194
  - [x] ELB
179
195
  - [x] EKS
180
196
  - [x] Elasticsearch
197
+ - [x] ElastiCache
181
198
  - [x] Firehose
182
199
  - [ ] FMS
183
200
  - [ ] Glacier
@@ -210,10 +227,29 @@ AWS Recon aims to collect all resources and metadata that are relevant in determ
210
227
 
211
228
  One of the primary motivations for AWS Recon was to build a tool that is easy to maintain and extend. If you feel like coverage could be improved for a particular service, we would welcome PRs to that effect. Anyone with a moderate familiarity with Ruby will be able to mimic the pattern used by the existing collectors to query a specific service and add the results to the resource collection.
212
229
 
230
+ ### Development
231
+
232
+ Clone this repository:
233
+
234
+ ```
235
+ $ git clone git@github.com:darkbitio/aws-recon.git
236
+ $ cd aws-recon
237
+ ```
238
+
239
+ Create a sticky gemset if using RVM:
240
+
241
+ ```
242
+ $ rvm use 2.6.5@aws_recon_dev --create --ruby-version
243
+ ```
244
+
245
+ Run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
246
+
247
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
248
+
213
249
  ### TODO
214
250
 
215
251
  - [ ] Optionally suppress AWS API errors instead of re-raising them
216
- - [ ] Package as a gem
252
+ - [x] Package as a gem
217
253
  - [ ] Test coverage with AWS SDK stubbed resources
218
254
 
219
255