aws_recon 0.3.1 → 0.4.0

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: 3f7ece5998d62559623442fbd5435c0a6949abb140c00a72007ab28b19cefa0a
4
- data.tar.gz: b7ebfdc20a895b60a29435b4f70456c73c99d99ae1a642e06bbab9a86f70fd5a
3
+ metadata.gz: 9647cee32c4df1624a99cf180258f43e664e7ee62b8ceee5fe00b8d6b31a2803
4
+ data.tar.gz: aca9a6383263ca7add32682df25a633ad9db031a50d2f38f319f903b6dd75c45
5
5
  SHA512:
6
- metadata.gz: 027ee08ba24948e23d96988410aea58b95803dbf3f6e54b496c6da20f36a6913b1e590ca434a0301bf19900cc540989759c38ea96cc4986270d239183effbcdb
7
- data.tar.gz: c714d6d005d95806a89fe9ff1b82ec4d5dfc12a05ace708505849a84acc594576a9b53b8843cd6b36cc9a0d49a7dc0ca2830a5ae48f0faa9ae21c2cd8cf63cd2
6
+ metadata.gz: 426acf5937d26974c7a7c5bc26e84487fe8b76cc28bf0f51a7302d05897356e7007b877e26a47cc3af7b54044ac45bec23b409b1b66bef0336aaa1216d340437
7
+ data.tar.gz: 790fd25c65d4fe49c2e3857f65cddb590ecfe9f481b923bcdf8def9007ddf1ccb0b16f616164e7472b43a75eebe77698146d3d2e46204af1085b276afedf2099
data/.rubocop.yml CHANGED
@@ -12,7 +12,6 @@ Layout/LineLength:
12
12
  Max: 100
13
13
  Style/FrozenStringLiteralComment:
14
14
  EnforcedStyle: always_true
15
- Safe: true
16
15
  SafeAutoCorrect: true
17
16
  Style/ClassAndModuleChildren:
18
17
  Enabled: false
@@ -6,7 +6,7 @@ module AwsRecon
6
6
  class CLI
7
7
  def initialize
8
8
  # parse options
9
- @options = Parser.parse ARGV.length < 1 ? %w[-h] : ARGV
9
+ @options = Parser.parse ARGV.empty? ? %w[-h] : ARGV
10
10
 
11
11
  # timing
12
12
  @starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
@@ -15,11 +15,11 @@ module AwsRecon
15
15
  @account_id = Aws::STS::Client.new.get_caller_identity.account
16
16
 
17
17
  # AWS services
18
- @aws_services = YAML.load(File.read(SERVICES_CONFIG_FILE), symbolize_names: true)
18
+ @aws_services = YAML.safe_load(File.read(SERVICES_CONFIG_FILE), symbolize_names: true)
19
19
 
20
20
  # User config services
21
21
  if @options.config_file
22
- user_config = YAML.load(File.read(@options.config_file), symbolize_names: true)
22
+ user_config = YAML.safe_load(File.read(@options.config_file), symbolize_names: true)
23
23
 
24
24
  @services = user_config[:services]
25
25
  @regions = user_config[:regions]
@@ -34,9 +34,9 @@ module AwsRecon
34
34
  # formatter
35
35
  @formatter = Formatter.new
36
36
 
37
- unless @options.stream_output
38
- puts "\nStarting collection with #{@options.threads} threads...\n"
39
- end
37
+ return unless @options.stream_output
38
+
39
+ puts "\nStarting collection with #{@options.threads} threads...\n"
40
40
  end
41
41
 
42
42
  #
@@ -72,7 +72,7 @@ module AwsRecon
72
72
  #
73
73
  # global services
74
74
  #
75
- @aws_services.map { |x| OpenStruct.new(x) }.filter { |s| s.global }.each do |service|
75
+ @aws_services.map { |x| OpenStruct.new(x) }.filter(&:global).each do |service|
76
76
  # user included this service in the args
77
77
  next unless @services.include?(service.name)
78
78
 
@@ -94,7 +94,7 @@ module AwsRecon
94
94
  next unless @regions.include?(region) && !skip_region
95
95
 
96
96
  # user included this service in the args
97
- 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)
98
98
 
99
99
  collect(service, region)
100
100
  end
@@ -104,14 +104,45 @@ module AwsRecon
104
104
 
105
105
  puts "\nStopped early after \x1b[32m#{elapsed.to_i}\x1b[0m seconds.\n"
106
106
  ensure
107
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @starting
108
+
109
+ puts "\nFinished in \x1b[32m#{elapsed.to_i}\x1b[0m seconds.\n\n"
110
+
107
111
  # write output file
108
112
  if @options.output_file
109
- elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @starting
110
-
111
- puts "\nFinished in \x1b[32m#{elapsed.to_i}\x1b[0m seconds. Saving resources to \x1b[32m#{@options.output_file}\x1b[0m.\n\n"
113
+ puts "Saving resources to \x1b[32m#{@options.output_file}\x1b[0m.\n\n"
112
114
 
113
115
  File.write(@options.output_file, @resources.to_json)
114
116
  end
117
+
118
+ # write output file to S3 bucket
119
+ if @options.s3
120
+ t = Time.now.utc
121
+
122
+ s3_full_object_path = "AWSRecon/#{t.year}/#{t.month}/#{t.day}/#{@account_id}_aws_recon_#{t.to_i}.json.gz"
123
+
124
+ begin
125
+ # get bucket name (and region if not us-east-1)
126
+ s3_bucket, s3_region = @options.s3.split(':')
127
+
128
+ # build IO object and gzip it
129
+ io = StringIO.new
130
+ gzip_data = Zlib::GzipWriter.new(io)
131
+ gzip_data.write(@resources.to_json)
132
+ gzip_data.close
133
+
134
+ # send it to S3
135
+ s3_client = Aws::S3::Client.new(region: s3_region || 'us-east-1')
136
+ s3_resource = Aws::S3::Resource.new(client: s3_client)
137
+ obj = s3_resource.bucket(s3_bucket).object(s3_full_object_path)
138
+ obj.put(body: io.string)
139
+
140
+ puts "Saving resources to S3 \x1b[32ms3://#{s3_bucket}/#{s3_full_object_path}\x1b[0m\n\n"
141
+ rescue Aws::S3::Errors::ServiceError => e
142
+ puts "\x1b[35mError!\x1b[0m - could not save output S3 bucket\n\n"
143
+ puts "#{e.message} - #{e.code}\n"
144
+ end
145
+ end
115
146
  end
116
147
  end
117
148
  end
@@ -1,2 +1,4 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # require all collectors
2
- Dir[File.join(__dir__, 'collectors', '*.rb')].each { |file| require file }
4
+ Dir[File.join(__dir__, 'collectors', '*.rb')].sort.each { |file| require file }
@@ -13,14 +13,20 @@ class EMR < Mapper
13
13
  #
14
14
  # get_block_public_access_configuration
15
15
  #
16
- @client.get_block_public_access_configuration.each do |response|
17
- log(response.context.operation_name)
16
+ begin
17
+ @client.get_block_public_access_configuration.each do |response|
18
+ log(response.context.operation_name)
18
19
 
19
- struct = OpenStruct.new(response.block_public_access_configuration.to_h)
20
- struct.type = 'configuration'
21
- struct.arn = "arn:aws:emr:#{@region}:#{@account}/block_public_access_configuration"
20
+ struct = OpenStruct.new(response.block_public_access_configuration.to_h)
21
+ struct.type = 'configuration'
22
+ struct.arn = "arn:aws:emr:#{@region}:#{@account}/block_public_access_configuration"
22
23
 
23
- resources.push(struct.to_h)
24
+ resources.push(struct.to_h)
25
+ end
26
+ rescue Aws::EMR::Errors::ServiceError => e
27
+ log_error(e.code)
28
+
29
+ raise e unless suppressed_errors.include?(e.code) && !@options.quit_on_exception
24
30
  end
25
31
 
26
32
  #
@@ -42,4 +48,12 @@ class EMR < Mapper
42
48
 
43
49
  resources
44
50
  end
51
+
52
+ private
53
+
54
+ def suppressed_errors
55
+ %w[
56
+ InvalidRequestException
57
+ ]
58
+ end
45
59
  end
@@ -91,6 +91,28 @@ class IAM < Mapper
91
91
  end
92
92
  end
93
93
 
94
+ #
95
+ # list_instance_profiles
96
+ #
97
+ @client.list_instance_profiles.each_with_index do |response, page|
98
+ log(response.context.operation_name, page)
99
+
100
+ # instance_profiles
101
+ response.instance_profiles.each do |profile|
102
+ struct = OpenStruct.new(profile.to_h)
103
+ struct.type = 'instance_profile'
104
+ struct.arn = profile.arn
105
+ struct.roles = []
106
+
107
+ profile.roles&.each do |role|
108
+ role.assume_role_policy_document = role.assume_role_policy_document.parse_policy
109
+ struct.roles.push(role.to_h)
110
+ end
111
+
112
+ resources.push(struct.to_h)
113
+ end
114
+ end
115
+
94
116
  #
95
117
  # get_account_password_policy
96
118
  #
@@ -48,6 +48,7 @@ class S3 < Mapper
48
48
  { func: 'get_bucket_policy', key: 'policy', field: 'policy' },
49
49
  { func: 'get_bucket_policy_status', key: 'public', field: 'policy_status' },
50
50
  { func: 'get_public_access_block', key: 'public_access_block', field: 'public_access_block_configuration' },
51
+ { func: 'get_object_lock_configuration', key: 'object_lock_configuration', field: 'object_lock_configuration' },
51
52
  { func: 'get_bucket_tagging', key: 'tagging', field: nil },
52
53
  { func: 'get_bucket_logging', key: 'logging', field: 'logging_enabled' },
53
54
  { func: 'get_bucket_versioning', key: 'versioning', field: nil },
@@ -66,7 +67,7 @@ class S3 < Mapper
66
67
  end
67
68
 
68
69
  rescue Aws::S3::Errors::ServiceError => e
69
- log_error(e.code)
70
+ log_error(bucket.name, op.func, e.code)
70
71
 
71
72
  raise e unless suppressed_errors.include?(e.code) && !@options.quit_on_exception
72
73
  end
@@ -90,6 +91,7 @@ class S3 < Mapper
90
91
  NoSuchWebsiteConfiguration
91
92
  ReplicationConfigurationNotFoundError
92
93
  NoSuchPublicAccessBlockConfiguration
94
+ ObjectLockConfigurationNotFoundError
93
95
  ]
94
96
  end
95
97
  end
@@ -6,6 +6,7 @@
6
6
  class Parser
7
7
  DEFAULT_CONFIG_FILE = nil
8
8
  DEFAULT_OUTPUT_FILE = File.expand_path(File.join(Dir.pwd, 'output.json')).freeze
9
+ DEFAULT_S3_PATH = nil
9
10
  SERVICES_CONFIG_FILE = File.join(File.dirname(__FILE__), 'services.yaml').freeze
10
11
  DEFAULT_FORMAT = 'aws'
11
12
  DEFAULT_THREADS = 8
@@ -15,6 +16,7 @@ class Parser
15
16
  :regions,
16
17
  :services,
17
18
  :config_file,
19
+ :s3,
18
20
  :output_file,
19
21
  :output_format,
20
22
  :threads,
@@ -43,6 +45,7 @@ class Parser
43
45
  aws_regions,
44
46
  aws_services.map { |service| service[:name] },
45
47
  DEFAULT_CONFIG_FILE,
48
+ DEFAULT_S3_PATH,
46
49
  DEFAULT_OUTPUT_FILE,
47
50
  DEFAULT_FORMAT,
48
51
  DEFAULT_THREADS,
@@ -93,6 +96,11 @@ class Parser
93
96
  args.config_file = config
94
97
  end
95
98
 
99
+ # write output file to S3 bucket
100
+ opts.on('-b', '--s3-bucket [BUCKET:REGION]', 'Write output file to S3 bucket (default: \'\')') do |bucket_with_region|
101
+ args.s3 = bucket_with_region
102
+ end
103
+
96
104
  # output file
97
105
  opts.on('-o', '--output [OUTPUT]', 'Specify output file (default: output.json)') do |output|
98
106
  args.output_file = File.expand_path(File.join(Dir.pwd, output))
@@ -13,11 +13,12 @@
13
13
  - name: CodeBuild
14
14
  alias: codebuild
15
15
  excluded_regions:
16
- - af-south-1
16
+ - ap-northeast-3
17
17
  - name: CodePipeline
18
18
  alias: codepipeline
19
19
  excluded_regions:
20
20
  - af-south-1
21
+ - ap-northeast-3
21
22
  - me-south-1
22
23
  - name: AutoScaling
23
24
  alias: autoscaling
@@ -40,17 +41,10 @@
40
41
  - ap-southeast-1
41
42
  - name: ElasticLoadBalancingV2
42
43
  alias: elbv2
43
- excluded_regions:
44
- - ap-southeast-1
45
44
  - name: ElastiCache
46
45
  alias: elasticache
47
46
  - name: EMR
48
47
  alias: emr
49
- excluded_regions:
50
- - ap-east-1
51
- - af-south-1
52
- - eu-south-1
53
- - me-south-1
54
48
  - name: IAM
55
49
  global: true
56
50
  alias: iam
@@ -96,11 +90,9 @@
96
90
  - name: SES
97
91
  alias: ses
98
92
  excluded_regions:
99
- - eu-north-1
100
- - eu-west-3
101
- - us-west-1
102
- - ap-east-1
103
93
  - af-south-1
94
+ - ap-east-1
95
+ - ap-northeast-3
104
96
  - eu-south-1
105
97
  - name: CloudWatch
106
98
  alias: cloudwatch
@@ -110,65 +102,78 @@
110
102
  alias: kafka
111
103
  excluded_regions:
112
104
  - af-south-1
105
+ - ap-northeast-3
113
106
  - name: SecretsManager
114
107
  alias: sm
115
108
  - name: SecurityHub
116
109
  alias: sh
110
+ excluded_regions:
111
+ - ap-northeast-3
117
112
  - name: Support
118
113
  global: true
119
114
  alias: support
120
115
  - name: SSM
121
116
  alias: ssm
122
- excluded_regions:
123
- - ap-southeast-1
124
117
  - name: GuardDuty
125
118
  alias: guardduty
119
+ excluded_regions:
120
+ - ap-northeast-3
126
121
  - name: Athena
127
122
  alias: athena
123
+ excluded_regions:
124
+ - ap-northeast-3
128
125
  - name: EFS
129
126
  alias: efs
127
+ excluded_regions:
128
+ - ap-northeast-3
130
129
  - name: Firehose
131
130
  alias: firehose
132
131
  - name: Lightsail
133
132
  alias: lightsail
134
133
  excluded_regions:
135
- - eu-north-1
136
- - us-west-1
137
- - sa-east-1
138
- - ap-east-1
139
134
  - af-south-1
135
+ - ap-east-1
136
+ - ap-northeast-3
137
+ - eu-north-1
140
138
  - eu-south-1
141
139
  - me-south-1
140
+ - sa-east-1
141
+ - us-west-1
142
142
  - name: WorkSpaces
143
143
  alias: workspaces
144
144
  excluded_regions:
145
- - eu-north-1
145
+ - af-south-1
146
+ - ap-east-1
147
+ - ap-northeast-3
146
148
  - ap-south-1
149
+ - eu-north-1
150
+ - eu-south-1
147
151
  - eu-west-3
152
+ - me-south-1
148
153
  - us-east-2
149
154
  - us-west-1
150
- - ap-east-1
151
- - af-south-1
152
- - eu-south-1
153
- - me-south-1
154
155
  - name: SageMaker
155
156
  alias: sagemaker
157
+ excluded_regions:
158
+ - ap-northeast-3
156
159
  - name: ServiceQuotas
157
160
  alias: servicequotas
158
161
  - name: Transfer
159
162
  alias: transfer
160
163
  excluded_regions:
161
- - ap-east-1
162
- - af-south-1
164
+ - ap-northeast-3
163
165
  - eu-south-1
164
- - me-south-1
165
166
  - name: DirectConnect
166
167
  alias: dc
167
168
  - name: DirectoryService
168
169
  alias: ds
170
+ excluded_regions:
171
+ - ap-northeast-3
169
172
  - name: DatabaseMigrationService
170
173
  alias: dms
171
174
  - name: XRay
172
175
  alias: xray
173
176
  - name: WAFV2
174
177
  alias: wafv2
178
+ excluded_regions:
179
+ - ap-northeast-3
@@ -1,3 +1,3 @@
1
1
  module AwsRecon
2
- VERSION = "0.3.1"
2
+ VERSION = "0.4.0"
3
3
  end
data/readme.md CHANGED
@@ -5,9 +5,9 @@
5
5
 
6
6
  A multi-threaded AWS inventory collection tool.
7
7
 
8
- The [creators](https://darkbit.io) of this tool have a recurring need to be able to efficiently collect a large amount of AWS resource attributes and metadata to help clients understand their cloud security posture.
8
+ This tool was created to facilitate efficient collection of a large amount of AWS resource attributes and metadata. It aims to collect nearly everything that is relevant to the security configuration and posture of an AWS environment.
9
9
 
10
- Existing tools (e.g. [AWS Config](https://aws.amazon.com/config)) that do some form of resource collection lack the coverage and specificity we needed. We also needed a tool that produced consistent output that was easily consumed by other tools/systems.
10
+ Existing tools (e.g. [AWS Config](https://aws.amazon.com/config)) that do some form of resource collection lack the coverage and specificity to accurately measure security posture (e.g. detailed attribute data and full policy documents).
11
11
 
12
12
  Enter AWS Recon, multi-threaded AWS inventory collection tool written in plain Ruby. Though Python tends to dominate the AWS tooling landscape, the [Ruby SDK](https://aws.amazon.com/sdk-for-ruby/) has a few convenient advantages over the [other](https://aws.amazon.com/sdk-for-node-js/) [AWS](https://aws.amazon.com/sdk-for-python/) [SDKs](https://aws.amazon.com/sdk-for-go/) we tested. Specifically, easy handling of automatic retries, paging of large responses, and - with some help - threading huge numbers of requests.
13
13
 
@@ -15,7 +15,7 @@ Enter AWS Recon, multi-threaded AWS inventory collection tool written in plain R
15
15
 
16
16
  - More complete resource coverage than available tools (especially for ECS & EKS)
17
17
  - More granular resource detail, including nested related resources in the output
18
- - Flexible output (console, JSON lines, plain JSON, file, standard out)
18
+ - Flexible output (console, JSON lines, plain JSON, file, S3 bucket, and standard out)
19
19
  - Efficient (multi-threaded, rate limited, automatic retries, and automatic result paging)
20
20
  - Easy to maintain and extend
21
21
 
@@ -54,13 +54,13 @@ To run locally, first install the gem:
54
54
 
55
55
  ```
56
56
  $ gem install aws_recon
57
- Fetching aws_recon-0.3.0.gem
57
+ Fetching aws_recon-0.4.0.gem
58
58
  Fetching aws-sdk-3.0.1.gem
59
59
  Fetching parallel-1.20.1.gem
60
60
  ...
61
61
  Successfully installed aws-sdk-3.0.1
62
62
  Successfully installed parallel-1.20.1
63
- Successfully installed aws_recon-0.3.0
63
+ Successfully installed aws_recon-0.4.0
64
64
  ```
65
65
 
66
66
  Or add it to your Gemfile using `bundle`:
@@ -72,7 +72,7 @@ Resolving dependencies...
72
72
  ...
73
73
  Using aws-sdk 3.0.1
74
74
  Using parallel-1.20.1
75
- Using aws_recon 0.3.0
75
+ Using aws_recon 0.4.0
76
76
  ```
77
77
 
78
78
  ## Usage
@@ -158,13 +158,37 @@ Finished in 46 seconds. Saving resources to output.json.
158
158
  #### Example command line options
159
159
 
160
160
  ```
161
+ # collect S3 and EC2 global resources, as well as us-east-1 and us-east-2
162
+
161
163
  $ AWS_PROFILE=<profile> aws_recon -s S3,EC2 -r global,us-east-1,us-east-2
162
164
  ```
163
165
 
164
166
  ```
167
+ # collect S3 and EC2 global resources, as well as us-east-1 and us-east-2
168
+
165
169
  $ AWS_PROFILE=<profile> aws_recon --services S3,EC2 --regions global,us-east-1,us-east-2
166
170
  ```
167
171
 
172
+ ```
173
+ # save output to S3 bucket
174
+
175
+ $ AWS_PROFILE=<profile> aws_recon \
176
+ --services S3,EC2 \
177
+ --regions global,us-east-1,us-east-2 \
178
+ --verbose \
179
+ --s3-bucket my-recon-bucket
180
+ ```
181
+
182
+ ```
183
+ # save output to S3 bucket with a home region other than us-east-1
184
+
185
+ $ AWS_PROFILE=<profile> aws_recon \
186
+ --services S3,EC2 \
187
+ --regions global,us-east-1,us-east-2 \
188
+ --verbose \
189
+ --s3-bucket my-recon-bucket:us-west-2
190
+ ```
191
+
168
192
  Example [OpenCSPM](https://github.com/OpenCSPM/opencspm) formatted (NDJSON) output.
169
193
 
170
194
  ```
@@ -225,7 +249,7 @@ Most users will want to limit collection to relevant services and regions. Runni
225
249
  ```
226
250
  $ aws_recon -h
227
251
 
228
- AWS Recon - AWS Inventory Collector (0.3.0)
252
+ AWS Recon - AWS Inventory Collector (0.4.0)
229
253
 
230
254
  Usage: aws_recon [options]
231
255
  -r, --regions [REGIONS] Regions to scan, separated by comma (default: all)
@@ -233,6 +257,7 @@ Usage: aws_recon [options]
233
257
  -s, --services [SERVICES] Services to scan, separated by comma (default: all)
234
258
  -x, --not-services [SERVICES] Services to skip, separated by comma (default: none)
235
259
  -c, --config [CONFIG] Specify config file for services & regions (e.g. config.yaml)
260
+ -b, --s3-bucket [BUCKET:REGION] Write output file to S3 bucket (default: '')
236
261
  -o, --output [OUTPUT] Specify output file (default: output.json)
237
262
  -f, --format [FORMAT] Specify output format (default: aws)
238
263
  -t, --threads [THREADS] Specify max threads (default: 8, max: 128)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aws_recon
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Josh Larsen
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2021-02-05 00:00:00.000000000 Z
12
+ date: 2021-03-30 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: aws-sdk
@@ -167,7 +167,6 @@ files:
167
167
  - ".github/workflows/smoke-test.yml"
168
168
  - ".gitignore"
169
169
  - ".rubocop.yml"
170
- - ".travis.yml"
171
170
  - Dockerfile
172
171
  - Gemfile
173
172
  - LICENSE.txt
data/.travis.yml DELETED
@@ -1,7 +0,0 @@
1
- ---
2
- sudo: false
3
- language: ruby
4
- cache: bundler
5
- rvm:
6
- - 2.6.5
7
- before_install: gem install bundler -v 1.17.3