s3-secure 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/.rspec +3 -0
  4. data/CHANGELOG.md +7 -0
  5. data/Gemfile +6 -0
  6. data/Gemfile.lock +89 -0
  7. data/Guardfile +19 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +56 -0
  10. data/Rakefile +14 -0
  11. data/exe/s3-secure +14 -0
  12. data/lib/s3-secure.rb +1 -0
  13. data/lib/s3_secure.rb +14 -0
  14. data/lib/s3_secure/abstract_base.rb +17 -0
  15. data/lib/s3_secure/autoloader.rb +27 -0
  16. data/lib/s3_secure/aws_services.rb +36 -0
  17. data/lib/s3_secure/batch.rb +25 -0
  18. data/lib/s3_secure/cli.rb +37 -0
  19. data/lib/s3_secure/command.rb +82 -0
  20. data/lib/s3_secure/completer.rb +159 -0
  21. data/lib/s3_secure/completer/script.rb +6 -0
  22. data/lib/s3_secure/completer/script.sh +10 -0
  23. data/lib/s3_secure/encryption.rb +27 -0
  24. data/lib/s3_secure/encryption/base.rb +4 -0
  25. data/lib/s3_secure/encryption/disable.rb +18 -0
  26. data/lib/s3_secure/encryption/enable.rb +42 -0
  27. data/lib/s3_secure/encryption/list.rb +28 -0
  28. data/lib/s3_secure/encryption/show.rb +18 -0
  29. data/lib/s3_secure/help.rb +9 -0
  30. data/lib/s3_secure/help/completion.md +20 -0
  31. data/lib/s3_secure/help/completion_script.md +3 -0
  32. data/lib/s3_secure/help/hello.md +5 -0
  33. data/lib/s3_secure/policy.rb +27 -0
  34. data/lib/s3_secure/policy/base.rb +4 -0
  35. data/lib/s3_secure/policy/checker.rb +15 -0
  36. data/lib/s3_secure/policy/document.rb +27 -0
  37. data/lib/s3_secure/policy/document/base.rb +15 -0
  38. data/lib/s3_secure/policy/document/force_ssl_only_access.rb +33 -0
  39. data/lib/s3_secure/policy/document/force_ssl_only_access_remove.rb +33 -0
  40. data/lib/s3_secure/policy/enforce.rb +36 -0
  41. data/lib/s3_secure/policy/list.rb +29 -0
  42. data/lib/s3_secure/policy/show.rb +19 -0
  43. data/lib/s3_secure/policy/unforce.rb +41 -0
  44. data/lib/s3_secure/version.rb +3 -0
  45. data/s3-secure.gemspec +33 -0
  46. data/spec/lib/cli_spec.rb +12 -0
  47. data/spec/lib/policy/checker_spec.rb +68 -0
  48. data/spec/lib/policy/document/force_ssl_remove_spec.rb +107 -0
  49. data/spec/lib/policy/document_spec.rb +68 -0
  50. data/spec/spec_helper.rb +29 -0
  51. metadata +252 -0
@@ -0,0 +1,15 @@
1
+ class S3Secure::Policy::Document
2
+ class Base
3
+ extend Memoist
4
+
5
+ def initialize(bucket, bucket_policy)
6
+ # @bucket_policy is existing document policy
7
+ @bucket, @bucket_policy = bucket, bucket_policy
8
+ end
9
+
10
+ def checker
11
+ S3Secure::Policy::Checker.new(@bucket_policy)
12
+ end
13
+ memoize :checker
14
+ end
15
+ end
@@ -0,0 +1,33 @@
1
+ class S3Secure::Policy::Document
2
+ class ForceSSLOnlyAccess < Base
3
+ def policy_document
4
+ if @bucket_policy.blank?
5
+ full_policy_document
6
+ else
7
+ updated_policy_document
8
+ end
9
+ end
10
+
11
+ def updated_policy_document
12
+ policy = JSON.load(@bucket_policy)
13
+ policy["Statement"] << ssl_enforce_statement unless checker.has?("ForceSSLOnlyAccess")
14
+ policy
15
+ end
16
+
17
+ def full_policy_document
18
+ {"Version"=>"2012-10-17",
19
+ "Statement"=>[ssl_enforce_statement]}
20
+ end
21
+
22
+ def ssl_enforce_statement
23
+ {
24
+ "Sid"=>"ForceSSLOnlyAccess",
25
+ "Effect"=>"Deny",
26
+ "Principal"=>"*",
27
+ "Action"=>"s3:GetObject",
28
+ "Resource"=>"arn:aws:s3:::#{@bucket}/*",
29
+ "Condition"=>{"Bool"=>{"aws:SecureTransport"=>"false"}}
30
+ }
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,33 @@
1
+ class S3Secure::Policy::Document
2
+ class ForceSSLOnlyAccessRemove < Base
3
+ def initialize(bucket, bucket_policy)
4
+ # @bucket_policy is existing document policy
5
+ @bucket, @bucket_policy = bucket, bucket_policy
6
+ end
7
+
8
+ def policy_document
9
+ return nil if @bucket_policy.blank?
10
+
11
+ updated_policy_document
12
+ end
13
+
14
+ def updated_policy_document
15
+ policy = JSON.load(@bucket_policy)
16
+
17
+ statements = policy["Statement"]
18
+ has_force_ssl = !!statements.detect { |s| s["Sid"] == "ForceSSLOnlyAccess" }
19
+ unless has_force_ssl
20
+ raise "Bucket policy does not have ForceSSLOnlyAccess"
21
+ end
22
+
23
+ if statements.size == 1
24
+ return nil # to signal for the entire bucket policy to be deleted
25
+ else
26
+ statements.delete_if { |s| s["Sid"] == "ForceSSLOnlyAccess" }
27
+ policy["Statement"] = statements
28
+ end
29
+
30
+ policy
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,36 @@
1
+ class S3Secure::Policy
2
+ class Enforce < Base
3
+ def initialize(options={})
4
+ super
5
+ @sid = options[:sid]
6
+ end
7
+
8
+ def run
9
+ @s3 = s3_regional_client(@bucket)
10
+
11
+ list = S3Secure::Policy::List.new(@options)
12
+ list.set_s3(@s3)
13
+
14
+ bucket_policy = list.get_policy(@bucket)
15
+ document = Document.new(@bucket, bucket_policy)
16
+ if document.has?(@sid)
17
+ puts "Bucket policy for #{@bucket} has ForceSSLOnlyAccess policy statement already:"
18
+ puts bucket_policy
19
+ else
20
+ # Set encryption rules
21
+ # Ruby docs: https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Client.html#put_bucket_policy-instance_method
22
+ # API docs: https://docs.aws.amazon.com/AmazonS3/latest/API/API_ServerSideEncryptionByDefault.html
23
+ #
24
+ # put_bucket_policy returns #<struct Aws::EmptyStructure>
25
+ #
26
+ policy_document = document.policy_document(@sid)
27
+ @s3.put_bucket_policy(
28
+ bucket: @bucket,
29
+ policy: policy_document,
30
+ )
31
+ puts "Add bucket policy to bucket #{@bucket}:"
32
+ puts policy_document
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,29 @@
1
+ class S3Secure::Policy
2
+ class List < Base
3
+ def run
4
+ buckets.each do |bucket|
5
+ @s3 = s3_regional_client(bucket)
6
+ puts "Policy for bucket #{bucket.color(:green)}"
7
+ policy = get_policy(bucket)
8
+
9
+ if policy
10
+ puts policy
11
+ else
12
+ puts "Bucket does not have a bucket policy"
13
+ end
14
+ 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
+
24
+ # Useful when calling List outside of the list CLI
25
+ def set_s3(client)
26
+ @s3 = client
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,19 @@
1
+ class S3Secure::Policy
2
+ class Show < Base
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
+ if policy
11
+ puts "Bucket #{@bucket} is configured with this policy:"
12
+ puts policy
13
+ # puts policy.map(&:to_h)
14
+ else
15
+ puts "Bucket #{@bucket} is not configured bucket policy"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,41 @@
1
+ class S3Secure::Policy
2
+ class Unforce < Base
3
+ def initialize(options={})
4
+ super
5
+ @sid = options[:sid]
6
+ end
7
+
8
+ def run
9
+ @s3 = s3_regional_client(@bucket)
10
+
11
+ list = S3Secure::Policy::List.new(@options)
12
+ list.set_s3(@s3)
13
+
14
+ bucket_policy = list.get_policy(@bucket)
15
+ document = Document.new(@bucket, bucket_policy, remove: true)
16
+ if document.has?(@sid)
17
+ # Set encryption rules
18
+ # Ruby docs: https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Client.html#put_bucket_policy-instance_method
19
+ # API docs: https://docs.aws.amazon.com/AmazonS3/latest/API/API_ServerSideEncryptionByDefault.html
20
+ #
21
+ # put_bucket_policy returns #<struct Aws::EmptyStructure>
22
+ #
23
+ policy_document = document.policy_document(@sid)
24
+
25
+ if policy_document
26
+ @s3.put_bucket_policy(
27
+ bucket: @bucket,
28
+ policy: policy_document,
29
+ )
30
+ else
31
+ @s3.delete_bucket_policy(bucket: @bucket)
32
+ end
33
+
34
+ puts "Remove bucket policy to bucket #{@bucket}:"
35
+ puts policy_document if policy_document
36
+ else
37
+ puts "Bucket policy for #{@bucket} does not have ForceSSLOnlyAccess policy statement. Nothing to be done."
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,3 @@
1
+ module S3Secure
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,33 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "s3_secure/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "s3-secure"
8
+ spec.version = S3Secure::VERSION
9
+ spec.authors = ["Tung Nguyen"]
10
+ spec.email = ["tongueroo@gmail.com"]
11
+ spec.summary = "S3 Bucket security hardening tool"
12
+ spec.homepage = "https://github.com/tongueroo/s3-secure"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files`.split($/)
16
+ spec.bindir = "exe"
17
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "activesupport"
22
+ spec.add_dependency "aws-sdk-s3"
23
+ spec.add_dependency "memoist"
24
+ spec.add_dependency "rainbow"
25
+ spec.add_dependency "thor"
26
+ spec.add_dependency "zeitwerk"
27
+
28
+ spec.add_development_dependency "bundler"
29
+ spec.add_development_dependency "byebug"
30
+ spec.add_development_dependency "cli_markdown"
31
+ spec.add_development_dependency "rake"
32
+ spec.add_development_dependency "rspec"
33
+ end
@@ -0,0 +1,12 @@
1
+ describe S3Secure::CLI do
2
+ before(:all) do
3
+ @args = "--from Tung"
4
+ end
5
+
6
+ describe "s3-secure" do
7
+ # it "hello" do
8
+ # out = execute("exe/s3-secure hello world #{@args}")
9
+ # expect(out).to include("from: Tung\nHello world")
10
+ # end
11
+ end
12
+ end
@@ -0,0 +1,68 @@
1
+ describe S3Secure::Policy::Checker do
2
+ subject { S3Secure::Policy::Checker.new(policy_json) }
3
+
4
+ describe "already has ForceSSLOnlyAccess" do
5
+ let(:policy_json) {
6
+ <<~JSON
7
+ {
8
+ "Version": "2012-10-17",
9
+ "Statement": [
10
+ {
11
+ "Sid": "ForceSSLOnlyAccess",
12
+ "Effect": "Deny",
13
+ "Principal": "*",
14
+ "Action": "s3:GetObject",
15
+ "Resource": "arn:aws:s3:::my-test-s3-secure-us-east-1/*",
16
+ "Condition": {
17
+ "Bool": {
18
+ "aws:SecureTransport": "false"
19
+ }
20
+ }
21
+ }
22
+ ]
23
+ }
24
+ JSON
25
+ }
26
+
27
+ it "has?" do
28
+ result = subject.has?("ForceSSLOnlyAccess")
29
+ expect(result).to be true
30
+ end
31
+ end
32
+
33
+ describe "doesnt have ForceSSLOnlyAccess" do
34
+ let(:policy_json) {
35
+ <<~JSON
36
+ {
37
+ "Version": "2008-10-17",
38
+ "Id": "PolicyForCloudFrontPrivateContent",
39
+ "Statement": [
40
+ {
41
+ "Sid": "1",
42
+ "Effect": "Allow",
43
+ "Principal": {
44
+ "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity E27G6OAEXAMPLE"
45
+ },
46
+ "Action": "s3:GetObject",
47
+ "Resource": "arn:aws:s3:::test-fake-website/*"
48
+ }
49
+ ]
50
+ }
51
+ JSON
52
+ }
53
+
54
+ it "has?" do
55
+ result = subject.has?("ForceSSLOnlyAccess")
56
+ expect(result).to be false
57
+ end
58
+ end
59
+
60
+ describe "empty policy" do
61
+ let(:policy_json) { nil }
62
+
63
+ it "has?" do
64
+ result = subject.has?("ForceSSLOnlyAccess")
65
+ expect(result).to be false
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,107 @@
1
+ describe S3Secure::Policy::Document::ForceSSLOnlyAccessRemove do
2
+ subject { S3Secure::Policy::Document::ForceSSLOnlyAccessRemove.new("my-bucket", policy_json) }
3
+
4
+ describe "already has ForceSSLOnlyAccess only" do
5
+ let(:policy_json) {
6
+ <<~JSON
7
+ {
8
+ "Version": "2012-10-17",
9
+ "Statement": [
10
+ {
11
+ "Sid": "ForceSSLOnlyAccess",
12
+ "Effect": "Deny",
13
+ "Principal": "*",
14
+ "Action": "s3:GetObject",
15
+ "Resource": "arn:aws:s3:::my-bucket/*",
16
+ "Condition": {
17
+ "Bool": {
18
+ "aws:SecureTransport": "false"
19
+ }
20
+ }
21
+ }
22
+ ]
23
+ }
24
+ JSON
25
+ }
26
+
27
+ it "policy_document" do
28
+ result = subject.policy_document
29
+ expect(result).to be nil
30
+ end
31
+ end
32
+
33
+ describe "already has ForceSSLOnlyAccess and another statement" do
34
+ let(:policy_json) {
35
+ <<~JSON
36
+ {
37
+ "Version": "2008-10-17",
38
+ "Id": "PolicyForCloudFrontPrivateContent",
39
+ "Statement": [
40
+ {
41
+ "Sid": "1",
42
+ "Effect": "Allow",
43
+ "Principal": {
44
+ "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity E27G6OAEXAMPLE"
45
+ },
46
+ "Action": "s3:GetObject",
47
+ "Resource": "arn:aws:s3:::test-fake-website/*"
48
+ },
49
+ {
50
+ "Sid": "ForceSSLOnlyAccess",
51
+ "Effect": "Deny",
52
+ "Principal": "*",
53
+ "Action": "s3:GetObject",
54
+ "Resource": "arn:aws:s3:::my-bucket/*",
55
+ "Condition": {
56
+ "Bool": {
57
+ "aws:SecureTransport": "false"
58
+ }
59
+ }
60
+ }
61
+ ]
62
+ }
63
+ JSON
64
+ }
65
+
66
+ it "policy_document" do
67
+ result = JSON.pretty_generate(subject.policy_document)
68
+ expect(result).not_to include("ForceSSLOnlyAccess")
69
+ expect(result).to include("PolicyForCloudFrontPrivateContent")
70
+ end
71
+ end
72
+
73
+ describe "empty policy" do
74
+ let(:policy_json) { nil }
75
+
76
+ it "policy_document" do
77
+ result = subject.policy_document
78
+ expect(result).to be nil
79
+ end
80
+ end
81
+
82
+ describe "doesnt have ForceSSLOnlyAccess" do
83
+ let(:policy_json) {
84
+ <<~JSON
85
+ {
86
+ "Version": "2008-10-17",
87
+ "Id": "PolicyForCloudFrontPrivateContent",
88
+ "Statement": [
89
+ {
90
+ "Sid": "1",
91
+ "Effect": "Allow",
92
+ "Principal": {
93
+ "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity E27G6OAEXAMPLE"
94
+ },
95
+ "Action": "s3:GetObject",
96
+ "Resource": "arn:aws:s3:::test-fake-website/*"
97
+ }
98
+ ]
99
+ }
100
+ JSON
101
+ }
102
+
103
+ it "policy_document" do
104
+ expect { subject.policy_document }.to raise_error(RuntimeError)
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,68 @@
1
+ describe S3Secure::Policy::Document do
2
+ subject { S3Secure::Policy::Document.new("my-bucket", policy_json) }
3
+
4
+ describe "already has ForceSSLOnlyAccess" do
5
+ let(:policy_json) {
6
+ <<~JSON
7
+ {
8
+ "Version": "2012-10-17",
9
+ "Statement": [
10
+ {
11
+ "Sid": "ForceSSLOnlyAccess",
12
+ "Effect": "Deny",
13
+ "Principal": "*",
14
+ "Action": "s3:GetObject",
15
+ "Resource": "arn:aws:s3:::my-bucket/*",
16
+ "Condition": {
17
+ "Bool": {
18
+ "aws:SecureTransport": "false"
19
+ }
20
+ }
21
+ }
22
+ ]
23
+ }
24
+ JSON
25
+ }
26
+
27
+ it "policy_document" do
28
+ result = subject.policy_document("ForceSSLOnlyAccess")
29
+ expect(result).to include("ForceSSLOnlyAccess")
30
+ end
31
+ end
32
+
33
+ describe "doesnt have ForceSSLOnlyAccess" do
34
+ let(:policy_json) {
35
+ <<~JSON
36
+ {
37
+ "Version": "2008-10-17",
38
+ "Id": "PolicyForCloudFrontPrivateContent",
39
+ "Statement": [
40
+ {
41
+ "Sid": "1",
42
+ "Effect": "Allow",
43
+ "Principal": {
44
+ "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity E27G6OAEXAMPLE"
45
+ },
46
+ "Action": "s3:GetObject",
47
+ "Resource": "arn:aws:s3:::test-fake-website/*"
48
+ }
49
+ ]
50
+ }
51
+ JSON
52
+ }
53
+
54
+ it "policy_document" do
55
+ result = subject.policy_document("ForceSSLOnlyAccess")
56
+ expect(result).to include("ForceSSLOnlyAccess")
57
+ end
58
+ end
59
+
60
+ describe "empty policy" do
61
+ let(:policy_json) { nil }
62
+
63
+ it "policy_document" do
64
+ result = subject.policy_document("ForceSSLOnlyAccess")
65
+ expect(result).to include("ForceSSLOnlyAccess")
66
+ end
67
+ end
68
+ end