outliers 0.0.0 → 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|