s3-secure 0.2.0 → 0.5.0

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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.md +16 -0
  4. data/LICENSE.txt +201 -22
  5. data/README.md +134 -16
  6. data/lib/s3_secure.rb +3 -2
  7. data/lib/s3_secure/access_logs.rb +30 -0
  8. data/lib/s3_secure/access_logs/base.rb +4 -0
  9. data/lib/s3_secure/access_logs/disable.rb +37 -0
  10. data/lib/s3_secure/access_logs/enable.rb +41 -0
  11. data/lib/s3_secure/access_logs/list.rb +25 -0
  12. data/lib/s3_secure/access_logs/show.rb +89 -0
  13. data/lib/s3_secure/aws_services.rb +1 -30
  14. data/lib/s3_secure/aws_services/s3.rb +54 -0
  15. data/lib/s3_secure/cli.rb +26 -0
  16. data/lib/s3_secure/command.rb +7 -0
  17. data/lib/s3_secure/encryption.rb +2 -0
  18. data/lib/s3_secure/encryption/disable.rb +4 -8
  19. data/lib/s3_secure/encryption/enable.rb +4 -8
  20. data/lib/s3_secure/encryption/list.rb +12 -16
  21. data/lib/s3_secure/encryption/show.rb +11 -6
  22. data/lib/s3_secure/help/batch.md +14 -0
  23. data/lib/s3_secure/help/encryption/disable.md +5 -0
  24. data/lib/s3_secure/help/encryption/enable.md +6 -0
  25. data/lib/s3_secure/help/encryption/list.md +5 -0
  26. data/lib/s3_secure/help/lifecycle/add.md +13 -0
  27. data/lib/s3_secure/help/lifecycle/list.md +22 -0
  28. data/lib/s3_secure/help/lifecycle/remove.md +5 -0
  29. data/lib/s3_secure/help/lifecycle/show.md +13 -0
  30. data/lib/s3_secure/help/policy/enforce_ssl.md +34 -0
  31. data/lib/s3_secure/help/policy/list.md +5 -0
  32. data/lib/s3_secure/help/policy/unforce_ssl.md +61 -0
  33. data/lib/s3_secure/help/summary.md +22 -0
  34. data/lib/s3_secure/lifecycle.rb +31 -0
  35. data/lib/s3_secure/lifecycle/add.rb +33 -0
  36. data/lib/s3_secure/lifecycle/base.rb +5 -0
  37. data/lib/s3_secure/lifecycle/builder.rb +47 -0
  38. data/lib/s3_secure/lifecycle/list.rb +24 -0
  39. data/lib/s3_secure/lifecycle/remove.rb +28 -0
  40. data/lib/s3_secure/lifecycle/show.rb +40 -0
  41. data/lib/s3_secure/policy.rb +2 -0
  42. data/lib/s3_secure/policy/document.rb +1 -1
  43. data/lib/s3_secure/policy/enforce.rb +3 -6
  44. data/lib/s3_secure/policy/list.rb +13 -17
  45. data/lib/s3_secure/policy/show.rb +8 -6
  46. data/lib/s3_secure/policy/unforce.rb +5 -8
  47. data/lib/s3_secure/remediate_all.rb +11 -0
  48. data/lib/s3_secure/summary.rb +13 -0
  49. data/lib/s3_secure/summary/item.rb +16 -0
  50. data/lib/s3_secure/summary/items.rb +65 -0
  51. data/lib/s3_secure/table.rb +18 -0
  52. data/lib/s3_secure/version.rb +1 -1
  53. data/lib/s3_secure/versioning.rb +29 -0
  54. data/lib/s3_secure/versioning/base.rb +4 -0
  55. data/lib/s3_secure/versioning/disable.rb +19 -0
  56. data/lib/s3_secure/versioning/enable.rb +19 -0
  57. data/lib/s3_secure/versioning/list.rb +24 -0
  58. data/lib/s3_secure/versioning/show.rb +27 -0
  59. data/s3-secure.gemspec +5 -2
  60. data/spec/lib/lifecycle/builder_spec.rb +85 -0
  61. metadata +71 -6
  62. data/Gemfile.lock +0 -89
  63. data/lib/s3_secure/help/hello.md +0 -5
@@ -2,6 +2,8 @@ module S3Secure
2
2
  class Policy < Command
3
3
  desc "list", "List bucket policies"
4
4
  long_desc Help.text("policy/list")
5
+ option :format, desc: "Format options: #{CliFormat.formats.join(', ')}"
6
+ option :policy, type: :boolean, desc: "Filter for policy: all, true, false"
5
7
  def list
6
8
  List.new(options).run
7
9
  end
@@ -10,7 +10,7 @@ class S3Secure::Policy
10
10
 
11
11
  # Returns JSON text
12
12
  # Currently only support adding ForceSSLOnlyAccess document policy.
13
- def policy_document(sid, remove: false)
13
+ def policy_document(sid)
14
14
  enforcer_class = "S3Secure::Policy::Document::#{sid}"
15
15
  enforcer_class += "Remove" if @remove
16
16
  enforcer_class = enforcer_class.constantize # IE: ForceSSLOnlyAccess or ForceSSLOnlyAccessRemove
@@ -6,12 +6,9 @@ class S3Secure::Policy
6
6
  end
7
7
 
8
8
  def run
9
- @s3 = s3_regional_client(@bucket)
9
+ show = S3Secure::Policy::Show.new(@options)
10
10
 
11
- list = S3Secure::Policy::List.new(@options)
12
- list.set_s3(@s3)
13
-
14
- bucket_policy = list.get_policy(@bucket)
11
+ bucket_policy = show.policy
15
12
  document = Document.new(@bucket, bucket_policy)
16
13
  if document.has?(@sid)
17
14
  puts "Bucket policy for #{@bucket} has ForceSSLOnlyAccess policy statement already:"
@@ -24,7 +21,7 @@ class S3Secure::Policy
24
21
  # put_bucket_policy returns #<struct Aws::EmptyStructure>
25
22
  #
26
23
  policy_document = document.policy_document(@sid)
27
- @s3.put_bucket_policy(
24
+ s3.put_bucket_policy(
28
25
  bucket: @bucket,
29
26
  policy: policy_document,
30
27
  )
@@ -1,29 +1,25 @@
1
1
  class S3Secure::Policy
2
2
  class List < Base
3
3
  def run
4
+ presenter = CliFormat::Presenter.new(@options)
5
+ presenter.header = ["Bucket", "Has Policy?"]
6
+
4
7
  buckets.each do |bucket|
5
- @s3 = s3_regional_client(bucket)
6
- puts "Policy for bucket #{bucket.color(:green)}"
7
- policy = get_policy(bucket)
8
+ $stderr.puts "Getting policy for bucket #{bucket.color(:green)}"
9
+ show = Show.new(bucket: bucket)
10
+ policy = show.policy
8
11
 
9
- if policy
10
- puts policy
12
+ row = [bucket, !!policy]
13
+ if @options[:policy].nil?
14
+ presenter.rows << row # always show policy
15
+ elsif @options[:policy]
16
+ presenter.rows << row if policy # only show if bucket has a policy
11
17
  else
12
- puts "Bucket does not have a bucket policy"
18
+ presenter.rows << row unless policy # only show if bucket doesnt have a policy
13
19
  end
14
20
  end
15
- end
16
-
17
- def get_policy(bucket)
18
- resp = @s3.get_bucket_policy(bucket: bucket)
19
- data = JSON.load(resp.policy.read) # String
20
- JSON.pretty_generate(data)
21
- rescue Aws::S3::Errors::NoSuchBucketPolicy
22
- end
23
21
 
24
- # Useful when calling List outside of the list CLI
25
- def set_s3(client)
26
- @s3 = client
22
+ presenter.show
27
23
  end
28
24
  end
29
25
  end
@@ -1,12 +1,6 @@
1
1
  class S3Secure::Policy
2
2
  class Show < Base
3
3
  def run
4
- @s3 = s3_regional_client(@bucket)
5
-
6
- list = S3Secure::Policy::List.new(@options)
7
- list.set_s3(@s3)
8
-
9
- policy = list.get_policy(@bucket)
10
4
  if policy
11
5
  puts "Bucket #{@bucket} is configured with this policy:"
12
6
  puts policy
@@ -15,5 +9,13 @@ class S3Secure::Policy
15
9
  puts "Bucket #{@bucket} is not configured bucket policy"
16
10
  end
17
11
  end
12
+
13
+ def policy
14
+ resp = s3.get_bucket_policy(bucket: @bucket)
15
+ data = JSON.load(resp.policy.read) # String
16
+ JSON.pretty_generate(data)
17
+ rescue Aws::S3::Errors::NoSuchBucketPolicy
18
+ end
19
+ memoize :policy
18
20
  end
19
21
  end
@@ -6,12 +6,9 @@ class S3Secure::Policy
6
6
  end
7
7
 
8
8
  def run
9
- @s3 = s3_regional_client(@bucket)
9
+ show = S3Secure::Policy::Show.new(@options)
10
10
 
11
- list = S3Secure::Policy::List.new(@options)
12
- list.set_s3(@s3)
13
-
14
- bucket_policy = list.get_policy(@bucket)
11
+ bucket_policy = show.policy
15
12
  document = Document.new(@bucket, bucket_policy, remove: true)
16
13
  if document.has?(@sid)
17
14
  # Set encryption rules
@@ -23,15 +20,15 @@ class S3Secure::Policy
23
20
  policy_document = document.policy_document(@sid)
24
21
 
25
22
  if policy_document
26
- @s3.put_bucket_policy(
23
+ s3.put_bucket_policy(
27
24
  bucket: @bucket,
28
25
  policy: policy_document,
29
26
  )
30
27
  else
31
- @s3.delete_bucket_policy(bucket: @bucket)
28
+ s3.delete_bucket_policy(bucket: @bucket)
32
29
  end
33
30
 
34
- puts "Remove bucket policy to bucket #{@bucket}:"
31
+ puts "Remove bucket policy statement from bucket #{@bucket}:"
35
32
  puts policy_document if policy_document
36
33
  else
37
34
  puts "Bucket policy for #{@bucket} does not have ForceSSLOnlyAccess policy statement. Nothing to be done."
@@ -0,0 +1,11 @@
1
+ module S3Secure
2
+ class RemediateAll < AbstractBase
3
+ def run
4
+ Encryption::Enable.new(bucket: @bucket).run
5
+ Policy::Enforce.new(bucket: @bucket, sid: "ForceSSLOnlyAccess").run
6
+ Versioning::Enable.new(bucket: @bucket).run
7
+ Lifecycle::Add.new(bucket: @bucket).run
8
+ AccessLogs::Enable.new(bucket: @bucket).run
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ module S3Secure
2
+ class Summary < AbstractBase
3
+ def run
4
+ $stderr.puts("Determining bucket security-related settings. Can take a while for lots of buckets...")
5
+ data = [%w[Bucket SSL? Encrypted?]]
6
+ items = Items.new(@options, buckets)
7
+ items.filtered_items.each do |i|
8
+ data << [i.bucket, i.ssl, i.encrypted]
9
+ end
10
+ S3Secure::Table.new(@options, data).display
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,16 @@
1
+ class S3Secure::Summary
2
+ class Item
3
+ attr_reader :bucket
4
+ def initialize(bucket, properties={})
5
+ @bucket, @properties = bucket, properties
6
+ end
7
+
8
+ def method_missing(name, *args, &block)
9
+ if @properties.key?(name)
10
+ @properties[name]
11
+ else
12
+ super
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,65 @@
1
+ class S3Secure::Summary
2
+ class Items < S3Secure::AbstractBase
3
+ extend Memoist
4
+
5
+ # override initialize
6
+ def initialize(options, buckets)
7
+ @options, @buckets = options, buckets
8
+ @ssl, @encrypted = @options[:ssl], @options[:encrypted]
9
+ end
10
+
11
+ def filtered_items
12
+ items = all_items.select do |item|
13
+ case @ssl
14
+ when "yes", "no"
15
+ @ssl == item.ssl
16
+ else # any or fallback
17
+ true
18
+ end
19
+ end
20
+
21
+ items.select do |item|
22
+ case @encrypted
23
+ when "yes", "no"
24
+ @encrypted == item.encrypted
25
+ else # any or fallback
26
+ true
27
+ end
28
+ end
29
+ end
30
+
31
+ # Triggers loading of items
32
+ def all_items
33
+ load_items!
34
+ end
35
+
36
+ def load_items!
37
+ @buckets.map do |bucket|
38
+ Item.new(bucket,
39
+ ssl: ssl?(bucket) ? "yes" : "no",
40
+ encrypted: encrypted?(bucket) ? "yes" : "no")
41
+ end
42
+ end
43
+ memoize :load_items!
44
+
45
+ private
46
+ def ssl?(bucket)
47
+ list = S3Secure::Policy::List.new(@options)
48
+
49
+ bucket_policy = list.get_policy(bucket)
50
+ document = S3Secure::Policy::Document.new(bucket, bucket_policy)
51
+ document.has?("ForceSSLOnlyAccess")
52
+ end
53
+ memoize :ssl?
54
+
55
+ def encrypted?(bucket)
56
+ s3 = s3_regional_client(bucket)
57
+ list = S3Secure::Encryption::List.new(@options)
58
+ list.set_s3(s3)
59
+
60
+ rules = list.get_encryption_rules(bucket)
61
+ !!rules
62
+ end
63
+ memoize :encrypted?
64
+ end
65
+ end
@@ -0,0 +1,18 @@
1
+ require "text-table"
2
+
3
+ module S3Secure
4
+ class Table
5
+ attr_reader :data
6
+ def initialize(options, data)
7
+ @options = options
8
+ @data = data
9
+ end
10
+
11
+ def display
12
+ table = Text::Table.new
13
+ table.head = data.shift
14
+ table.rows = data
15
+ puts table
16
+ end
17
+ end
18
+ end
@@ -1,3 +1,3 @@
1
1
  module S3Secure
2
- VERSION = "0.2.0"
2
+ VERSION = "0.5.0"
3
3
  end
@@ -0,0 +1,29 @@
1
+ module S3Secure
2
+ class Versioning < Command
3
+ desc "list", "List bucket versionings"
4
+ long_desc Help.text("versioning/list")
5
+ option :format, desc: "Format options: #{CliFormat.formats.join(', ')}"
6
+ option :versioning, desc: "Filter for versioning: all, true, false"
7
+ def list
8
+ List.new(options).run
9
+ end
10
+
11
+ desc "show BUCKET", "show bucket versioning"
12
+ long_desc Help.text("versioning/show")
13
+ def show(bucket)
14
+ Show.new(options.merge(bucket: bucket)).run
15
+ end
16
+
17
+ desc "enable BUCKET", "enable bucket versioning"
18
+ long_desc Help.text("versioning/enable")
19
+ def enable(bucket)
20
+ Enable.new(options.merge(bucket: bucket)).run
21
+ end
22
+
23
+ desc "disable BUCKET", "disable bucket versioning"
24
+ long_desc Help.text("versioning/disable")
25
+ def disable(bucket)
26
+ Disable.new(options.merge(bucket: bucket)).run
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,4 @@
1
+ class S3Secure::Versioning
2
+ class Base < S3Secure::AbstractBase
3
+ end
4
+ end
@@ -0,0 +1,19 @@
1
+ class S3Secure::Versioning
2
+ class Disable < Base
3
+ def run
4
+ show = Show.new(@options)
5
+ if show.enabled?
6
+ s3.put_bucket_versioning(
7
+ bucket: @bucket,
8
+ versioning_configuration: {
9
+ # mfa_delete: "Disabled",
10
+ status: "Suspended",
11
+ },
12
+ )
13
+ puts "Versioning Suspended on bucket #{@bucket}"
14
+ else
15
+ puts "Bucket #{@bucket} is already has versioning already Suspended or not Enabled."
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ class S3Secure::Versioning
2
+ class Enable < Base
3
+ def run
4
+ show = Show.new(@options)
5
+ if show.enabled?
6
+ puts "Bucket #{@bucket} is has versioning already enabled."
7
+ else
8
+ s3.put_bucket_versioning(
9
+ bucket: @bucket,
10
+ versioning_configuration: {
11
+ # mfa_delete: "Disabled",
12
+ status: "Enabled",
13
+ },
14
+ )
15
+ puts "Versioning enabled on bucket #{@bucket}"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,24 @@
1
+ class S3Secure::Versioning
2
+ class List < Base
3
+ def run
4
+ presenter = CliFormat::Presenter.new(@options)
5
+ presenter.header = ["Bucket", "Has Versioning?"]
6
+
7
+ buckets.each do |bucket|
8
+ $stderr.puts "Getting versioning for bucket #{bucket.color(:green)}"
9
+
10
+ show = Show.new(bucket: bucket)
11
+ row = [bucket, show.enabled?]
12
+ if @options[:versioning].nil?
13
+ presenter.rows << row # always show policy
14
+ elsif @options[:versioning]
15
+ presenter.rows << row if show.enabled? # only show if bucket has some encryption rules
16
+ else
17
+ presenter.rows << row unless show.enabled? # only show if bucket doesnt have any encryption rules
18
+ end
19
+ end
20
+
21
+ presenter.show
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,27 @@
1
+ class S3Secure::Versioning
2
+ class Show < Base
3
+ def run
4
+ if enabled?
5
+ puts "This S3 bucket has versioning enabled"
6
+ else
7
+ puts "This S3 bucket does not have versioning enabled"
8
+ end
9
+ details = get_versioning(@bucket).to_h
10
+ unless details.empty?
11
+ puts "Bucket versioning details: "
12
+ pp details
13
+ end
14
+ end
15
+
16
+ def enabled?
17
+ versioning = get_versioning(@bucket)
18
+ versioning.status == "Enabled" # Can be Enabled, Suspended, or nil
19
+ end
20
+
21
+ def get_versioning(bucket)
22
+ s3.get_bucket_versioning(bucket: bucket) # resp
23
+ rescue Aws::S3::Errors::ServerSideEncryptionConfigurationNotFoundError
24
+ end
25
+ memoize :get_versioning
26
+ end
27
+ end
@@ -10,9 +10,10 @@ Gem::Specification.new do |spec|
10
10
  spec.email = ["tongueroo@gmail.com"]
11
11
  spec.summary = "S3 Bucket security hardening tool"
12
12
  spec.homepage = "https://github.com/tongueroo/s3-secure"
13
- spec.license = "MIT"
13
+ spec.license = "Apache2.0"
14
14
 
15
- spec.files = `git ls-files`.split($/)
15
+ git_installed = system("type git > /dev/null 2>&1")
16
+ spec.files = git_installed ? `git ls-files`.split($/) : Dir.glob("**/*")
16
17
  spec.bindir = "exe"
17
18
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
18
19
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
@@ -20,8 +21,10 @@ Gem::Specification.new do |spec|
20
21
 
21
22
  spec.add_dependency "activesupport"
22
23
  spec.add_dependency "aws-sdk-s3"
24
+ spec.add_dependency "cli-format"
23
25
  spec.add_dependency "memoist"
24
26
  spec.add_dependency "rainbow"
27
+ spec.add_dependency "text-table"
25
28
  spec.add_dependency "thor"
26
29
  spec.add_dependency "zeitwerk"
27
30
 
@@ -0,0 +1,85 @@
1
+ describe S3Secure::Lifecycle::Builder do
2
+ subject { S3Secure::Lifecycle::Builder.new(rules) }
3
+
4
+ describe "already has s3-secure-automated-cleanup rule" do
5
+ let(:rules) {
6
+ [{:expiration=>{:expired_object_delete_marker=>true},
7
+ :id=>"s3-secure-automated-cleanup",
8
+ :status=>"Enabled",
9
+ :noncurrent_version_expiration=>{:noncurrent_days=>365},
10
+ :abort_incomplete_multipart_upload=>{:days_after_initiation=>30}}]
11
+ }
12
+
13
+ it "has?" do
14
+ result = subject.has?("s3-secure-automated-cleanup")
15
+ expect(result).to be true
16
+ end
17
+
18
+ it "rules_with_addition" do
19
+ rules = subject.rules_with_addition
20
+ expect(rules.size).to eq 1 # no dups
21
+ result = has_lifecycle?(rules)
22
+ expect(result).to be true
23
+ end
24
+
25
+ it "rules_with_removal" do
26
+ rules = subject.rules_with_removal
27
+ result = has_lifecycle?(rules)
28
+ expect(result).to be false
29
+ end
30
+ end
31
+
32
+ describe "doesnt have s3-secure-automated-cleanup rule" do
33
+ let(:rules) {
34
+ [{:rules=>
35
+ [{:expiration=>{:expired_object_delete_marker=>true},
36
+ :id=>"someother-policy",
37
+ :status=>"Enabled",
38
+ :noncurrent_version_expiration=>{:noncurrent_days=>365},
39
+ :abort_incomplete_multipart_upload=>{:days_after_initiation=>30}}]}]
40
+ }
41
+
42
+ it "has?" do
43
+ result = subject.has?("s3-secure-automated-cleanup")
44
+ expect(result).to be false
45
+ end
46
+
47
+ it "rules_with_addition" do
48
+ rules = subject.rules_with_addition
49
+ expect(rules.size).to eq 2 # no dups
50
+ result = has_lifecycle?(rules)
51
+ expect(result).to be true
52
+ end
53
+
54
+ it "rules_with_removal" do
55
+ rules = subject.rules_with_removal
56
+ result = has_lifecycle?(rules)
57
+ expect(result).to be false
58
+ end
59
+ end
60
+
61
+ describe "empty policy" do
62
+ let(:rules) { nil }
63
+
64
+ it "has?" do
65
+ result = subject.has?("s3-secure-automated-cleanup")
66
+ expect(result).to be false
67
+ end
68
+
69
+ it "rules_with_addition" do
70
+ rules = subject.rules_with_addition
71
+ result = has_lifecycle?(rules)
72
+ expect(result).to be true
73
+ end
74
+
75
+ it "rules_with_removal" do
76
+ rules = subject.rules_with_removal
77
+ result = has_lifecycle?(rules)
78
+ expect(result).to be false
79
+ end
80
+ end
81
+
82
+ def has_lifecycle?(rules)
83
+ !!rules.detect { |rule| rule[:id] == S3Secure::Lifecycle::Builder::RULE_ID }
84
+ end
85
+ end