outliers 0.0.0 → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +5 -0
- data/CHANGELOG.md +3 -0
- data/LICENSE.txt +10 -19
- data/README.md +175 -8
- data/Rakefile +7 -0
- data/bin/outliers +6 -0
- data/lib/outliers/cli/evaluate.rb +115 -0
- data/lib/outliers/cli/process.rb +56 -0
- data/lib/outliers/cli/providers.rb +29 -0
- data/lib/outliers/cli/resources.rb +57 -0
- data/lib/outliers/cli.rb +78 -0
- data/lib/outliers/collection.rb +124 -0
- data/lib/outliers/credentials.rb +28 -0
- data/lib/outliers/evaluation.rb +85 -0
- data/lib/outliers/exceptions.rb +40 -0
- data/lib/outliers/mixins.rb +27 -0
- data/lib/outliers/provider.rb +32 -0
- data/lib/outliers/providers/aws/base.rb +33 -0
- data/lib/outliers/providers/aws/cloud_formation.rb +20 -0
- data/lib/outliers/providers/aws/ec2.rb +20 -0
- data/lib/outliers/providers/aws/elb.rb +20 -0
- data/lib/outliers/providers/aws/iam.rb +20 -0
- data/lib/outliers/providers/aws/rds.rb +20 -0
- data/lib/outliers/providers/aws/s3.rb +20 -0
- data/lib/outliers/providers/aws/sqs.rb +20 -0
- data/lib/outliers/providers/aws.rb +9 -0
- data/lib/outliers/providers/github.rb +23 -0
- data/lib/outliers/providers.rb +19 -0
- data/lib/outliers/resource.rb +34 -0
- data/lib/outliers/resources/aws/cloud_formation/stack.rb +10 -0
- data/lib/outliers/resources/aws/cloud_formation/stack_collection.rb +15 -0
- data/lib/outliers/resources/aws/ec2/instance.rb +75 -0
- data/lib/outliers/resources/aws/ec2/instance_collection.rb +15 -0
- data/lib/outliers/resources/aws/ec2/security_group.rb +28 -0
- data/lib/outliers/resources/aws/ec2/security_group_collection.rb +15 -0
- data/lib/outliers/resources/aws/elb/load_balancer.rb +51 -0
- data/lib/outliers/resources/aws/elb/load_balancer_collection.rb +15 -0
- data/lib/outliers/resources/aws/iam/user.rb +40 -0
- data/lib/outliers/resources/aws/iam/user_collection.rb +15 -0
- data/lib/outliers/resources/aws/rds/db_instance.rb +35 -0
- data/lib/outliers/resources/aws/rds/db_instance_collection.rb +15 -0
- data/lib/outliers/resources/aws/rds/db_snapshot.rb +13 -0
- data/lib/outliers/resources/aws/rds/db_snapshot_collection.rb +15 -0
- data/lib/outliers/resources/aws/s3/bucket.rb +73 -0
- data/lib/outliers/resources/aws/s3/bucket_collection.rb +18 -0
- data/lib/outliers/resources/aws/sqs/queue.rb +13 -0
- data/lib/outliers/resources/aws/sqs/queue_collection.rb +15 -0
- data/lib/outliers/resources/aws.rb +18 -0
- data/lib/outliers/resources/github/repo.rb +24 -0
- data/lib/outliers/resources/github/repo_collection.rb +13 -0
- data/lib/outliers/resources/github.rb +2 -0
- data/lib/outliers/resources.rb +12 -0
- data/lib/outliers/result.rb +24 -0
- data/lib/outliers/run.rb +40 -0
- data/lib/outliers/verifications/shared.rb +31 -0
- data/lib/outliers/verifications.rb +1 -0
- data/lib/outliers/version.rb +1 -1
- data/lib/outliers.rb +24 -1
- data/outliers.gemspec +9 -5
- data/spec/collection_spec.rb +103 -0
- data/spec/credentials_spec.rb +42 -0
- data/spec/evaluation_spec.rb +96 -0
- data/spec/fixtures/credentials1.yml +5 -0
- data/spec/fixtures/credentials2.yml +5 -0
- data/spec/helpers/fixtures.rb +8 -0
- data/spec/mixins_spec.rb +33 -0
- data/spec/provider_spec.rb +35 -0
- data/spec/providers_spec.rb +18 -0
- data/spec/resource_spec.rb +19 -0
- data/spec/resources_spec.rb +15 -0
- data/spec/results_spec.rb +33 -0
- data/spec/run_spec.rb +56 -0
- data/spec/spec_helper.rb +21 -0
- data/spec/verifications/shared_spec.rb +35 -0
- metadata +145 -29
@@ -0,0 +1,75 @@
|
|
1
|
+
module Outliers
|
2
|
+
module Resources
|
3
|
+
module Aws
|
4
|
+
module Ec2
|
5
|
+
class Instance < Resource
|
6
|
+
def self.key
|
7
|
+
'instance_id'
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.verifications
|
11
|
+
[
|
12
|
+
{ name: 'classic',
|
13
|
+
description: 'Instance is in AWS Classic (No VPC).' },
|
14
|
+
{ name: 'source_dest_check',
|
15
|
+
description: 'Instance source dest check set to true.' },
|
16
|
+
{ name: 'running',
|
17
|
+
description: 'Instance status is running.' },
|
18
|
+
{ name: 'valid_image_id',
|
19
|
+
description: 'ami_ids=ami_id1,ami_id2 - Instances Image ID (AMI) is in given list.',
|
20
|
+
args: 'image_ids: [IMAGE_ID1, IMAGEID2]' },
|
21
|
+
{ name: 'vpc',
|
22
|
+
description: 'Instance is in a VPC.' }
|
23
|
+
]
|
24
|
+
end
|
25
|
+
|
26
|
+
def classic?
|
27
|
+
!vpc?
|
28
|
+
end
|
29
|
+
|
30
|
+
def running?
|
31
|
+
logger.debug "Verifying '#{status}' equals 'running'."
|
32
|
+
status == :running
|
33
|
+
end
|
34
|
+
|
35
|
+
def source_dest_check?
|
36
|
+
unless vpc?
|
37
|
+
logger.debug "Instance must be in a VPC to validate source_dest_check. Returning false."
|
38
|
+
return false
|
39
|
+
end
|
40
|
+
source_dest_check == true
|
41
|
+
end
|
42
|
+
|
43
|
+
def valid_image_id?(args)
|
44
|
+
image_ids = Array(args[:image_ids])
|
45
|
+
|
46
|
+
logger.debug "Verifying Image ID '#{image_id}' is one of '#{image_ids.join(', ')}'."
|
47
|
+
image_ids.include? image_id
|
48
|
+
end
|
49
|
+
|
50
|
+
def vpc?
|
51
|
+
!source.vpc_id.nil?
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def tags
|
57
|
+
@tags ||= source.tags
|
58
|
+
end
|
59
|
+
|
60
|
+
def image_id
|
61
|
+
@image_id ||= source.image_id
|
62
|
+
end
|
63
|
+
|
64
|
+
def instance_type
|
65
|
+
@instance_type ||= source.instance_type
|
66
|
+
end
|
67
|
+
|
68
|
+
def source_dest_check
|
69
|
+
@source_dest_check ||= source.source_dest_check
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Outliers
|
2
|
+
module Resources
|
3
|
+
module Aws
|
4
|
+
module Ec2
|
5
|
+
class SecurityGroup < Resource
|
6
|
+
def self.verifications
|
7
|
+
[
|
8
|
+
{ name: 'no_public_internet_ingress',
|
9
|
+
description: 'Security Group has no rules open to "0.0.0.0/0".' }
|
10
|
+
]
|
11
|
+
end
|
12
|
+
|
13
|
+
def no_public_internet_ingress?
|
14
|
+
logger.debug "Verifying '#{id}'."
|
15
|
+
source.ip_permissions.select do |i|
|
16
|
+
if !i.egress? && (i.ip_ranges.include? "0.0.0.0/0")
|
17
|
+
logger.debug "Security Group '#{id}' is open to '#{i.ip_ranges.join(', ')}' via '#{i.protocol}'."
|
18
|
+
false
|
19
|
+
else
|
20
|
+
true
|
21
|
+
end
|
22
|
+
end.any?
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Outliers
|
2
|
+
module Resources
|
3
|
+
module Aws
|
4
|
+
module Elb
|
5
|
+
class LoadBalancer < Resource
|
6
|
+
def self.verifications
|
7
|
+
[
|
8
|
+
{ name: 'ssl_certificates_valid',
|
9
|
+
description: 'Validates all SSL certificates associated with an ELB are valid for given number of days',
|
10
|
+
args: 'days: DAYS' }
|
11
|
+
]
|
12
|
+
end
|
13
|
+
|
14
|
+
def ssl_certificates_valid?(args)
|
15
|
+
days = args[:days]
|
16
|
+
pass = true
|
17
|
+
|
18
|
+
logger.debug "Load Balancer '#{id}' has no certificates." unless certificates.any?
|
19
|
+
|
20
|
+
date = Time.now + (days.to_i * 86400)
|
21
|
+
|
22
|
+
logger.debug "Validating no certs expire before '#{date.to_s}'."
|
23
|
+
|
24
|
+
certificates.each do |c|
|
25
|
+
certificate = OpenSSL::X509::Certificate.new c.certificate_body
|
26
|
+
subject = certificate.subject
|
27
|
+
not_after = certificate.not_after
|
28
|
+
|
29
|
+
logger.debug "Certificate '#{subject}' expires '#{not_after}'."
|
30
|
+
result = not_after > date
|
31
|
+
logger.debug "Certificate #{result ? "valid" : "invalid"}."
|
32
|
+
pass = false unless result
|
33
|
+
end
|
34
|
+
pass
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def certificates
|
40
|
+
listeners.map {|l| l.server_certificate}.reject {|s| s.nil?}
|
41
|
+
end
|
42
|
+
|
43
|
+
def listeners
|
44
|
+
@listeners ||= source.listeners
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Outliers
|
2
|
+
module Resources
|
3
|
+
module Aws
|
4
|
+
module Iam
|
5
|
+
class User < Resource
|
6
|
+
def self.verifications
|
7
|
+
[
|
8
|
+
{ name: 'mfa_enabled',
|
9
|
+
description: 'Verify MFA enabled for user.' },
|
10
|
+
{ name: 'no_access_keys',
|
11
|
+
description: 'Verify user has no access keys.' },
|
12
|
+
{ name: 'no_password_set',
|
13
|
+
description: 'Verify password not set for user.' }
|
14
|
+
]
|
15
|
+
end
|
16
|
+
|
17
|
+
def no_access_keys?
|
18
|
+
logger.debug "#{id} has #{access_keys.count} access key(s)."
|
19
|
+
!access_keys.any?
|
20
|
+
end
|
21
|
+
|
22
|
+
def no_password_set?
|
23
|
+
!source.login_profile.exists?
|
24
|
+
end
|
25
|
+
|
26
|
+
def mfa_enabled?
|
27
|
+
source.mfa_devices.count > 0
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def access_keys
|
33
|
+
@access_keys ||= source.access_keys
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Outliers
|
2
|
+
module Resources
|
3
|
+
module Aws
|
4
|
+
module Rds
|
5
|
+
class DbInstance < Resource
|
6
|
+
def self.key
|
7
|
+
'db_instance_identifier'
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.verifications
|
11
|
+
[
|
12
|
+
{ name: 'backup_retention_period',
|
13
|
+
description: 'Validate the backup retention period equals given days for the db_instance.',
|
14
|
+
args: 'days: DAYS' },
|
15
|
+
{ name: 'multi_az',
|
16
|
+
description: 'RDS Multi AZ set to yes.' }
|
17
|
+
]
|
18
|
+
end
|
19
|
+
|
20
|
+
def backup_retention_period?(args)
|
21
|
+
days = args[:days]
|
22
|
+
|
23
|
+
current = source.backup_retention_period
|
24
|
+
logger.debug "Verifying '#{id}' retention period of '#{current}' equals '#{days}' days."
|
25
|
+
current.to_i == days.to_i
|
26
|
+
end
|
27
|
+
|
28
|
+
def multi_az?
|
29
|
+
source.multi_az?
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Outliers
|
2
|
+
module Resources
|
3
|
+
module Aws
|
4
|
+
module S3
|
5
|
+
class Bucket < Resource
|
6
|
+
|
7
|
+
def self.verifications
|
8
|
+
[
|
9
|
+
{ name: 'empty',
|
10
|
+
description: 'Bucket has no objects.' },
|
11
|
+
{ name: 'no_public_objects',
|
12
|
+
description: 'Bucket has no public accessible objects.' },
|
13
|
+
{ name: 'configured_as_website',
|
14
|
+
description: 'Bucket is configured as a website.' },
|
15
|
+
{ name: 'not_configured_as_website',
|
16
|
+
description: 'Bucket is not configured as a website.' }
|
17
|
+
]
|
18
|
+
end
|
19
|
+
|
20
|
+
def empty?
|
21
|
+
logger.debug "Bucket #{id} has #{count} objects."
|
22
|
+
|
23
|
+
count == 0
|
24
|
+
end
|
25
|
+
|
26
|
+
def no_public_objects?
|
27
|
+
passed = true
|
28
|
+
|
29
|
+
logger.info "Validating #{objects.count} objects in '#{id}' are private."
|
30
|
+
|
31
|
+
objects.each do |o|
|
32
|
+
logger.debug "Verifying '#{o.key}' is private."
|
33
|
+
o.acl.grants.select do |g|
|
34
|
+
grantee = Nokogiri::XML(g.grantee.to_s).children.children.children.to_s
|
35
|
+
if grantee == "http://acs.amazonaws.com/groups/global/AllUsers" || grantee == "http://acs.amazonaws.com/groups/global/AuthenticatedUsers"
|
36
|
+
logger.debug "Object '#{o.key}' in '#{id}' has public grant '#{grantee}'."
|
37
|
+
passed = false
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
logger.debug "Verification of '#{id}' #{passed ? 'passed' : 'failed'}."
|
43
|
+
|
44
|
+
passed
|
45
|
+
end
|
46
|
+
|
47
|
+
def not_configured_as_website?
|
48
|
+
!configured_as_website?
|
49
|
+
end
|
50
|
+
|
51
|
+
def configured_as_website?
|
52
|
+
!website_configuration.nil?
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def website_configuration
|
58
|
+
source.website_configuration
|
59
|
+
end
|
60
|
+
|
61
|
+
def count
|
62
|
+
objects.count
|
63
|
+
end
|
64
|
+
|
65
|
+
def objects
|
66
|
+
@objects ||= source.objects
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Outliers
|
2
|
+
module Resources
|
3
|
+
module Aws
|
4
|
+
module S3
|
5
|
+
class BucketCollection < Collection
|
6
|
+
|
7
|
+
def load_all
|
8
|
+
unless provider.credentials['region'] == 'us-east-1'
|
9
|
+
raise Exceptions::UnsupportedRegion.new "Bucket verifications must target region us-east-1."
|
10
|
+
end
|
11
|
+
connect.buckets.map {|r| resource_class.new r}
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'outliers/resources/aws/cloud_formation/stack'
|
2
|
+
require 'outliers/resources/aws/cloud_formation/stack_collection'
|
3
|
+
require 'outliers/resources/aws/ec2/security_group'
|
4
|
+
require 'outliers/resources/aws/ec2/security_group_collection'
|
5
|
+
require 'outliers/resources/aws/ec2/instance'
|
6
|
+
require 'outliers/resources/aws/ec2/instance_collection'
|
7
|
+
require 'outliers/resources/aws/elb/load_balancer'
|
8
|
+
require 'outliers/resources/aws/elb/load_balancer_collection'
|
9
|
+
require 'outliers/resources/aws/iam/user'
|
10
|
+
require 'outliers/resources/aws/iam/user_collection'
|
11
|
+
require 'outliers/resources/aws/rds/db_instance'
|
12
|
+
require 'outliers/resources/aws/rds/db_instance_collection'
|
13
|
+
require 'outliers/resources/aws/rds/db_snapshot'
|
14
|
+
require 'outliers/resources/aws/rds/db_snapshot_collection'
|
15
|
+
require 'outliers/resources/aws/s3/bucket'
|
16
|
+
require 'outliers/resources/aws/s3/bucket_collection'
|
17
|
+
require 'outliers/resources/aws/sqs/queue'
|
18
|
+
require 'outliers/resources/aws/sqs/queue_collection'
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Outliers
|
2
|
+
module Resources
|
3
|
+
module Github
|
4
|
+
class Repo < Resource
|
5
|
+
def self.verifications
|
6
|
+
[
|
7
|
+
{ name: 'private',
|
8
|
+
description: 'Repo is private.' },
|
9
|
+
{ name: 'public',
|
10
|
+
description: 'Repo is public.' }
|
11
|
+
]
|
12
|
+
end
|
13
|
+
|
14
|
+
def private?
|
15
|
+
source.private
|
16
|
+
end
|
17
|
+
|
18
|
+
def public?
|
19
|
+
!source.private
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Outliers
|
2
|
+
class Result
|
3
|
+
|
4
|
+
attr_reader :description, :passed
|
5
|
+
|
6
|
+
def initialize(args)
|
7
|
+
@description = args[:description]
|
8
|
+
@passed = args[:passed]
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_s
|
12
|
+
passed? ? 'passed' : 'failed'
|
13
|
+
end
|
14
|
+
|
15
|
+
def passed?
|
16
|
+
@passed == true
|
17
|
+
end
|
18
|
+
|
19
|
+
def failed?
|
20
|
+
!passed?
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
data/lib/outliers/run.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
module Outliers
|
2
|
+
class Run
|
3
|
+
attr_accessor :credentials, :results
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@results = []
|
7
|
+
end
|
8
|
+
|
9
|
+
def process_evaluations_in_config_folder
|
10
|
+
evaluations_path = File.join Outliers.config_path
|
11
|
+
entries = Dir.entries(evaluations_path) - ['.', '..']
|
12
|
+
entries.each do |e|
|
13
|
+
file = File.join(evaluations_path, e)
|
14
|
+
unless File.directory? file
|
15
|
+
logger.info "Processing '#{file}'."
|
16
|
+
self.instance_eval File.read(file)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def evaluate(name='unspecified')
|
22
|
+
yield Evaluation.new :name => name, :run => self
|
23
|
+
end
|
24
|
+
|
25
|
+
def passed
|
26
|
+
@results.select {|r| r.passed?}
|
27
|
+
end
|
28
|
+
|
29
|
+
def failed
|
30
|
+
@results.reject {|r| r.passed?}
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def logger
|
36
|
+
@logger ||= Outliers.logger
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Outliers
|
2
|
+
module Verifications
|
3
|
+
module Shared
|
4
|
+
|
5
|
+
def none_exist?
|
6
|
+
logger.debug 'Verifying no resources exist.'
|
7
|
+
logger.debug "Found #{all.empty? ? 'no resources' : all_by_key.join(',')}."
|
8
|
+
all.empty?
|
9
|
+
end
|
10
|
+
|
11
|
+
def equals?(args)
|
12
|
+
list = Array(args[:keys])
|
13
|
+
logger.debug "Verifying '#{list.join(',')}' equals #{all.empty? ? 'no resources' : all_by_key.join(',')}."
|
14
|
+
list == all_by_key
|
15
|
+
end
|
16
|
+
|
17
|
+
module_function
|
18
|
+
|
19
|
+
def verifications
|
20
|
+
[
|
21
|
+
{ name: 'none_exist',
|
22
|
+
description: 'Verify no resources exist.' },
|
23
|
+
{ name: 'equals',
|
24
|
+
description: 'Verify resources match the given list of keys.',
|
25
|
+
args: 'keys: [KEY1,KEY2]' }
|
26
|
+
]
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'outliers/verifications/shared'
|
data/lib/outliers/version.rb
CHANGED
data/lib/outliers.rb
CHANGED
@@ -1,5 +1,28 @@
|
|
1
|
+
require "outliers/mixins.rb"
|
2
|
+
require "outliers/verifications"
|
3
|
+
|
4
|
+
require "outliers/collection"
|
5
|
+
require "outliers/credentials"
|
6
|
+
require "outliers/exceptions"
|
7
|
+
require "outliers/evaluation"
|
8
|
+
require "outliers/provider"
|
9
|
+
require "outliers/providers"
|
10
|
+
require "outliers/resource"
|
11
|
+
require "outliers/resources"
|
12
|
+
require "outliers/result"
|
13
|
+
require "outliers/run"
|
14
|
+
|
1
15
|
require "outliers/version"
|
2
16
|
|
3
17
|
module Outliers
|
4
|
-
|
18
|
+
module_function
|
19
|
+
|
20
|
+
def logger(logger=nil)
|
21
|
+
@logger ||= logger ? logger : Logger.new(STDOUT)
|
22
|
+
end
|
23
|
+
|
24
|
+
def config_path(path=nil)
|
25
|
+
@config_path ||= path ? path : './'
|
26
|
+
end
|
27
|
+
|
5
28
|
end
|