From 5ab68ecf0329a596674aa26d3592cdac4db745ee Mon Sep 17 00:00:00 2001 From: Clinton Wolfe Date: Fri, 19 Jan 2018 11:50:08 -0500 Subject: [PATCH] aws_s3_bucket with modified interface (#183) Signed-off-by: Matthew Dromazos Signed-off-by: Aaron Lippold Signed-off-by: Sam Cornwell <14048146+samcornwell@users.noreply.github.com> Signed-off-by: Clinton Wolfe --- docs/resources/aws_s3_bucket.md | 123 ++++++++ libraries/_aws_connection.rb | 4 + libraries/aws_s3_bucket.rb | 100 ++++++ .../integration/default/build/inspec-logo.png | Bin 0 -> 8501 bytes test/integration/default/build/s3.tf | 100 ++++++ .../verify/controls/aws_iam_root_user.rb | 1 - .../default/verify/controls/aws_iam_user.rb | 1 - .../default/verify/controls/aws_s3_bucket.rb | 113 +++++++ test/unit/resources/aws_s3_bucket_test.rb | 289 ++++++++++++++++++ test/unit/resources/aws_vpc.notes | 91 ++++++ 10 files changed, 820 insertions(+), 2 deletions(-) create mode 100644 docs/resources/aws_s3_bucket.md create mode 100644 libraries/aws_s3_bucket.rb create mode 100644 test/integration/default/build/inspec-logo.png create mode 100644 test/integration/default/build/s3.tf create mode 100644 test/integration/default/verify/controls/aws_s3_bucket.rb create mode 100644 test/unit/resources/aws_s3_bucket_test.rb create mode 100644 test/unit/resources/aws_vpc.notes diff --git a/docs/resources/aws_s3_bucket.md b/docs/resources/aws_s3_bucket.md new file mode 100644 index 000000000..5f43e4e71 --- /dev/null +++ b/docs/resources/aws_s3_bucket.md @@ -0,0 +1,123 @@ +--- +title: About the aws_s3_bucket Resource +--- + +# aws_s3_bucket + +Use the `aws_s3_bucket` InSpec audit resource to test properties of a single AWS bucket. + +To test properties of a multiple S3 buckets, use the `aws_s3_buckets` resource. + +
+ +## Limitations + +S3 bucket security is a complex matter. For details on how AWS evaluates requests for access, please see [the AWS documentation](https://docs.aws.amazon.com/AmazonS3/latest/dev/how-s3-evaluates-access-control.html). S3 buckets and the objects they contain support three different types of access control: bucket ACLs, bucket policies, and object ACLs. + +As of January 2018, this resource supports evaluating bucket ACLs and bucket policies. We do not support evaluating object ACLs because it introduces scalability concerns in the AWS API; we recommend using AWS mechanisms such as CloudTrail and Config to detect insecure object ACLs. + +In particular, users of the `be_public` matcher should carefully examine the conditions under which the matcher will detect an insecure bucket. See the `be_public` section under the Matchers section below. + +## Syntax + +An `aws_s3_bucket` resource block declares a bucket by name, and then lists tests to be performed. + + describe aws_s3_bucket(bucket_name: 'test_bucket') do + it { should exist } + it { should_not be_public } + end + + describe aws_s3_bucket('test_bucket') do + it { should exist } + end + +
+ +## Examples + +The following examples show how to use this InSpec audit resource. + +### Test a bucket's bucket-level ACL + + describe aws_s3_bucket('test_bucket') do + its('bucket_acl.count') { should eq 1 } + end + +### Check to see if a bucket has a bucket policy + + describe aws_s3_bucket('test_bucket') do + its('bucket_policy') { should be_empty } + end + +### Check to see if a bucket appears to be exposed to the public + + # See Limitations section above + describe aws_s3_bucket('test_bucket') do + it { should_not be_public } + end +
+ +## Supported Properties + +### region + +The `region` property identifies the AWS Region in which the S3 bucket is located. + + describe aws_s3_bucket('test_bucket') do + # Check if the correct region is set + its('region') { should eq 'us-east-1' } + end + +## Unsupported Properties + +### bucket_acl + +The `bucket_acl` property is a low-level property that lists the individual Bucket ACL grants that are in effect on the bucket. Other higher-level properties, such as be\_public, are more concise and easier to use. You can use the `bucket_acl` property to investigate which grants are in effect, causing be\_public to fail. + +The value of bucket_acl is an Array of simple objects. Each object has a `permission` property and a `grantee` property. The `permission` property will be a string such as 'READ', 'WRITE' etc (See the [AWS documentation](https://docs.aws.amazon.com/sdkforruby/api/Aws/S3/Client.html#get_bucket_acl-instance_method) for a full list). The `grantee` property contains sub-properties, such as `type` and `uri`. + + + bucket_acl = aws_s3_bucket('my-bucket') + + # Look for grants to "AllUsers" (that is, the public) + all_users_grants = bucket_acl.select do |g| + g.grantee.type == 'Group' && g.grantee.uri =~ /AllUsers/ + end + + # Look for grants to "AuthenticatedUsers" (that is, any authenticated AWS user - nearly public) + auth_grants = bucket_acl.select do |g| + g.grantee.type == 'Group' && g.grantee.uri =~ /AuthenticatedUsers/ + end + +### bucket_policy + +The `bucket_policy` is a low-level property that describes the IAM policy document controlling access to the bucket. The `bucket_policy` property returns a Ruby structure that you can probe to check for particular statements. We recommend using a higher-level property, such as `be_public`, which is concise and easier to implement in your policy files. + +The `bucket_policy` property returns an Array of simple objects, each object being an IAM Policy Statement. See the [AWS documentation](https://docs.aws.amazon.com/AmazonS3/latest/dev/example-bucket-policies.html#example-bucket-policies-use-case-2) for details about the structure of this data. + +If there is no bucket policy, this property will return an empty Array. + + bucket_policy = aws_s3_bucket('my-bucket') + + # Look for statements that allow the general public to do things + # This may be a false positive; it's possible these statements + # could be protected by conditions, such as IP restrictions. + public_statements = bucket_policy.select do |s| + s.effect == 'Allow' && s.principal == '*' + end + +## Matchers + +This InSpec audit resource has the following special matchers. For a full list of available matchers (such as `exist`) please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/). + +### be_public + +The `be_public` matcher tests if the bucket has potentially insecure access controls. This high-level matcher detects several insecure conditions, which may be enhanced in the future. Currently, the matcher reports an insecure bucket if any of the following conditions are met: + + 1. A bucket ACL grant exists for the 'AllUsers' group + 2. A bucket ACL grant exists for the 'AuthenticatedUsers' group + 3. A bucket policy has an effect 'Allow' and principal '*' + +Note: This resource does not detect insecure object ACLs. + + it { should_not be_public } diff --git a/libraries/_aws_connection.rb b/libraries/_aws_connection.rb index ad5ad91dd..6922dbb37 100644 --- a/libraries/_aws_connection.rb +++ b/libraries/_aws_connection.rb @@ -52,4 +52,8 @@ class AWSConnection def iam_client @iam_client ||= Aws::IAM::Client.new end + + def s3_client + @s3_client ||= Aws::S3::Client.new + end end diff --git a/libraries/aws_s3_bucket.rb b/libraries/aws_s3_bucket.rb new file mode 100644 index 000000000..db9584b27 --- /dev/null +++ b/libraries/aws_s3_bucket.rb @@ -0,0 +1,100 @@ +# author: Matthew Dromazos +class AwsS3Bucket < Inspec.resource(1) + name 'aws_s3_bucket' + desc 'Verifies settings for a s3 bucket' + example " + describe aws_s3_bucket(bucket_name: 'test_bucket') do + it { should exist } + end + " + + include AwsResourceMixin + attr_reader :bucket_name, :region + + def to_s + "S3 Bucket #{@bucket_name}" + end + + def bucket_acl + # This is simple enough to inline it. + @bucket_acl ||= AwsS3Bucket::BackendFactory.create.get_bucket_acl(bucket: bucket_name).grants + end + + def bucket_policy + @bucket_policy ||= fetch_bucket_policy + end + + # RSpec will alias this to be_public + def public? + # first line just for formatting + false || \ + bucket_acl.any? { |g| g.grantee.type == 'Group' && g.grantee.uri =~ /AllUsers/ } || \ + bucket_acl.any? { |g| g.grantee.type == 'Group' && g.grantee.uri =~ /AuthenticatedUsers/ } || \ + bucket_policy.any? { |s| s.effect == 'Allow' && s.principal == '*' } + end + + private + + def validate_params(raw_params) + validated_params = check_resource_param_names( + raw_params: raw_params, + allowed_params: [:bucket_name], + allowed_scalar_name: :bucket_name, + allowed_scalar_type: String, + ) + if validated_params.empty? or !validated_params.key?(:bucket_name) + raise ArgumentError, 'You must provide a bucket_name to aws_s3_bucket.' + end + + validated_params + end + + def fetch_from_aws + backend = AwsS3Bucket::BackendFactory.create + + # Since there is no basic "get_bucket" API call, use the + # region fetch as the existence check. + begin + @region = backend.get_bucket_location(bucket: bucket_name).location_constraint + rescue Aws::S3::Errors::NoSuchBucket + @exists = false + return + end + @exists = true + end + + def fetch_bucket_policy + backend = AwsS3Bucket::BackendFactory.create + + begin + # AWS SDK returns a StringIO, we have to read() + raw_policy = backend.get_bucket_policy(bucket: bucket_name).policy + return JSON.parse(raw_policy.read)['Statement'].map do |statement| + lowercase_hash = {} + statement.each_key { |k| lowercase_hash[k.downcase] = statement[k] } + OpenStruct.new(lowercase_hash) + end + rescue Aws::S3::Errors::NoSuchBucketPolicy + return [] + end + end + + # Uses the SDK API to really talk to AWS + class Backend + class AwsClientApi + BackendFactory.set_default_backend(self) + + def get_bucket_acl(query) + AWSConnection.new.s3_client.get_bucket_acl(query) + end + + def get_bucket_location(query) + AWSConnection.new.s3_client.get_bucket_location(query) + end + + def get_bucket_policy(query) + AWSConnection.new.s3_client.get_bucket_policy(query) + end + end + end +end diff --git a/test/integration/default/build/inspec-logo.png b/test/integration/default/build/inspec-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..4fe193b5d45468e9ee3099c58eca4abc64cd06da GIT binary patch literal 8501 zcmX9^2UrtL6Anc{Ku`e{5ov;SlqS6h2m$Fuk)|SqNRv+JMWnY7-~&l$i6|nycLk*r z2}%pyKnO)JL3;TQ|2$8UyV=><-MibFciy>p6C>RVjMo`KAkYP{p0*hXL_G`~S2EB6 zzdSZ8wZPx`S9;byAdsZM*_SF1qwWs^@q)nGnihdm8%RU{sowF}@QsY7nx?q%(Z$v%ApwPlCRX1Azof-+$d(oSVOi%OaIfI_Qq(~o(V`^S@B*QD z^l>S?6xHnaakXJ{u7hdz-Cfl9t)I7k3L+;rU3c==;6|)R{kmYW!{cvitUCd3bt;+) ze^3E)J6gEcy>AUJH%NFG=L(l(YDO$sp#=pK?R|7MfMyWyuug*ai=rDG!hNy5zR&rQ zTMRV+#F9Eq&hTltFUQ`-G&na$&odBxX73_V{6&-C2MJOUx}hnH-5{55I6L%4i@S1peBoQI{KL4qkJ2k!fC`*43S#)NLK|4=ELf{H)5av@M zdyr@HZ-ASLH99$tmYegJ=Ne@qg$|ej#kiVS+zoJUe3wDUf~i^JiVPsL$YEo}z=TO$ zxOI*ac)Rt?nMI4sp8Uhfe_G3B<)XD79s%<{Xe|^58Vd9SFP+Uo0(>5KgZ3)i$_jF( zALo;TQ%T!y&>LciHwTB?%$!|Pi>NPVauagJvLTtiUFBSWBtoGjV_6&RjEJd2Gr)i+ zEkZgYxb%$#wJqNeX7Rq+b3Z0P%S7KhS>f$tLH_)vG8NHel?vgrr&O4$NhdFTS^t$N zKlYy~fuRVMv)i$lBuhg+Tb&6)L13s?=HGFg434??-|`r4x4fKC8xjK(+3&`MqJW7N z(7w3oHLEyH3wF21zV{#_F z$e**MaPJ!>3*8}NHW96UrENLs`I&Zqyo|C-XQYaLsZ`L=1s$sS+enWNp$|eL)g1|_ zeF)d#`bn4^GU+h`GiVd)xeDL0zFG(0u5=H7mtnftrm!haSN77La%>b^wV`pdOgdmP zl;NdveyL=UEnqJQY`-V`t76blMQP^5AbGUQSNch2WBPFOu>5GB3tM20oARfu;{%jN zZ*g5@yehV)a{#kEB6#k&4E7im1CL#S?J?(BbFt-c*(q&RyB}t}n8^<9omZygfG;1q z>*!612MuZ6+#_Uu?%7%RfRDB~m(%W&MC{V3fSE@H?`Qql4Gr0V_5HVCkZe_g-+Xwx zxDkzY81wUYb$=Ge{TQ!iUH_|O$JTBNn~qin%YbV-`El;xeASQlL}r$Ke(AH6TA`GY z{KHlKb=S5`Nwt3KqeXIT{hzeQ5k@{Ui1+K*Pt$nWd%ZFW8-sLY&x9jeoCU6Obc6L> z#--pkNEZg}$slXt`!vktaFnzYyUf=WO1GuE@yku*x>J1EtidX9KWcO;6t`d{v4F#~ z-p-}LFo`MoZ#d0Qky}4|!kk~RIYX>AeGj?zk&l8zNfky79Se#+{NJ_S%G1B%A4Gbn zXKj0G;ErmK_r;;W7<0A(ew7L~s~Vg|)Tu&)bvhNCvAzq5_LOHMuHO>vYlKL~E!p4W z72#a4H-pNxT;NmTymLKx29xK??HckS@LKQ6x6BqTp^7y*WhOEEbyOjU90f5$Hippg zSR3%?^Ax7bsutHHPcH9$%EQ;3>y>#i`r#}5gmIfJC3p;sPp_>nfY>MZ`vX9dez^+r!hfANa0y<)7%*0E0OK#+buJW??dV#%xRFo4%uuz>TPRn z{hDQ|U^P$*)Mi-x_TJn64!_@-xb5Vzk;SrC0V9EkZDP@QQh&&knWAx9iJ&1p=)r^c zl?s5HLBq1$w{Hs_UCq$d8SXRlgk^By>R2D?`B5*II6v7gad5gmYi^Vlh?e$d#=qwd(T6re|up zFuS}z{!b@8;yAz->i3=VrcFXpQ)7P+gsubxH+wI&9 zQJ%7AO<4Ds>}D^h%}r`D^P!)coTb>1Na2{wFB#L2K#Y65<)#K*40viMMeVZLNN z9>HT%%#S2h+MxHobHudzPsJx`kcs@qKjY~n6iby4iLpmsf&7CI;JJNA#y;Qpg&6t$ zS=ck7M&~V7tNF7|1-SU&qe_py`Mvw;M4#Xt5bVvka%24$+i-6;F04$JY6*Ph-hw*R zO6jAI<)5NGo!s5akQg@iS$#@E&xsDq{+tljH?3C&QIb3ywcsl~WwK<{c*}_mwM>9V1ypGAfyr7kz zbEUmR0_g=pWtV3*K_T|L9^cSX7R0FOJ}Poo>p7h$>oCkyOkzkYeN-Im0QkxyTzH6S z-Ue;M#RuH(=zSL$OW|nSqO2x?6SYBDEgkk<9nUUOPgA5xD1dK(ZPrm$?ymNJ#TlIr zC>AEFIYDgImx}?Q`k6UYF zp4$ELm(>EBOS$t#tAk?629%tm8B-?-r~nin;I=LR#>?yOtmVvR3s%iG7#?r+8~mc6 z3S6~C%{|sXQ~aiOYww~OIEBv*d_bc|H*|Y_-XW0#yME9wx$BfvLpQcVH`W)EEP->Q z+2Rc)ewp#47u)ztf9 z*Y&PX)ShnXMG9$ z)V?kgyV(~F-UmFauKa)Ds}1*J>t%TC1Fb`xNL{8=@{KN&dd_ns9ECfl9lkx;zSeQ$ zOA+Qf8OhpI(h7|aeUJ`GyYRhcS>6>IhKs!5mO2s2k4b&56h5aJf1}Sw`+Rn8WCFOa z?NRQPbiz^jEjT~h%0$RI)^Vzp=>O9t*mfkw-o{ZS_4=>V86bdWq1Cq!)X45vXn#v#;q8rq#u9?G`fuvmblg+} zl#xj}+lC&L$nsmEu1Q3&DYZNF zj)O`lo#ezQCyefP8HL^QE$UO-_c3uhqq!_Z}onRXyGOEeW;9#aCLw!!BWk+^# z-8J`{BNfM|Z-MssOUo*hb}7RG$hr8Uj}H`>_^HUu))~RR0ZN>z6Ph$UnMQLSP;>6s zY)r8lJfP;qU}c;U^eLc+x70F-GN#xd;|_COk$q@V5YM^1Xb|1A`CxC`vKns0$O#q8V` zy6neF;P3nmVf_>hb=}AizRSE=lhWP(B5btu@5wwzP7lK7L%u+b;gRKG8{J3rQo}dJ z^~FMbU)T%?k}2$MYu^Ift6c|~c$BNmQ}Zn~P+Bp)&lP)vlv*F8XC<_1k4>_u+Q-)% zB@tAYIoxN{mzh%6*6y3Zi&c8aGEtEV0$9u2Yag&o_;HR1D$Da#+-Brn(Y@7C{!Y;Z zAIVEt4YXnatlk#FqnjKjL$>v^6|Y@DsA=CzB{C=QrS1l+B9o?^qf>O)X-*H?jrV~`v!g>)70Cl?Gr4OP8(q!%%8PrV!^2n4O?~Ef zdcX3FB^JFGm~d4(M&%K*u1orKjI`sJV#`94DBP^~3~$nm>HMqFXtpL$zqoGm>p#Ml z%=7&!W1lO?yiuwMiOa28c~r0+#&Mb{SMX`BMh4w{h3xo}i&f>rk~Yhu{Z~2fs(ck_ zmg^J_>nHl5u}>9eoSr=O&(uOSdzwP7Fz{pJeAH|W4q8Sx)Oknj^-cNjX%+|y+A`X< zd@gu-?tm{Adg7?}ex>-g2xFsB3MUA;6w0m~$cL>1yh;!=fP#OLcEE+-JQf zuVIUIDo6xA`|aGf-vYy=cEx;H3x5}X*ODaP;KRakvY())?kS;a0nNw37!tLYrPbKg zTLW?!rZx|6mqfcE7j3v&c57)V|8SRRdn^yZq5SF@}V3$y;)o98DT1 zb0CaO1F%mttnFJ<-*+N_-DD$EU=|MS^ICXPF8RWfX_-Wm+{mnf`e#;Oy^ncMQ-1gg zg=J-#El*E;1oo=WwJHa`;Ruyr#iytsU)5~5q~S^|KjX0(=RR{Z%6q;#wY6e3ICQZv zq_HecRQ#nEEcGL!9OJ7S0s6*i{pKjK(5JCF{+p_;wdNSpmGH?aNrj6iD?Z6ke#j9P#wuv^kJ&tek-7B-WGG``dt8bmjKU|j(C&P>F zrC^tipVAQhG_u`+!eou~!UnlA#0E$&e6{X^UW*J%W(<8qu&#OU zroA4go8`kcD0a|iW)Vp**ue*vCom2fyLnu$mcK%1B5(;ZAcV~_9lV_%+myARj80{A z)x_34Udf4-^&ty^7MVe)L|ZP{aN8goh6$~epym+Aww~q;>gEKcK?qF|(&3dWlugFl zyZZ@w#6fHRA<)t#NH+eGC3H?R`y)xfo=XE2ExDVyq!!MsT~IOUE+FGkl5q*gyoq_E za2P$n;%;6TgfXQh>~p~05k|K!u&tbm7%l{E?|V-Y_wA+)4qRNB3zQnKUwd@0RV9jf zX#p0xN3DT!$bjsbp1cK?EQ3rMzIFV}Z=qPb=f~J*AUT&FpD5w1M)%WL&viE~Z?$~% zEk6+Yt`zlhfRYOrZoTO|+V{RqV8lVCG&|9#LA@Yr=c+SUu4pjx8)nyH?3|HGqWIZE znp~fe&?>$gC4GqxI}6?(@|7+%I8bLJwM0p+y^&b8clxwlP&nl)eLZc!7G^sZZt?T} z@lyf#0Arkw6aO%fz=$rk7TSqtzE=hAVJ}@6+C0$e9^4u3EL83ezSpll{>K9MJxmQ9 zS<$UlLwZqre%=~DH70&dX?h1q3J|GbwS}@vz{Y0p$Dal&0(jW_h{fbpI%M9W=WY)x0c1Of0%@3U^%tnEjA zYrf0X9-=>lL1bpnaZ7#?QH=sm_E$gPt&Y7UMLf_=7b7Dl=`zXNY?c=&%u{y}Q_Z+2 z-f#$b@~sS`gF?NojJnsC0KjJrPfQ>n|dUF%;b8$O_Ve_jPRf#d{-=|#A5i*7e3 zkhm5NKgWD#9p6}2JXE4#mmIz-kGD|t+HoJrThkY&)PA|M)vTGfpa}CA*z8iA2O|vu z1cFw}}7JnOVTdUtg`sMZRIx z_Eo>3(#O*C5{9|*(ILruZEVe`!QrD>4oU$vwOglo15qhSA35H4H=5T9A%L9^Xd6OE z5_~?N3s$08k6HU`=Y{E>4ErlVjrSI}jqlG~3n2z>Iurf8X$=6l!S}5IA8hiILHpw=8L%sz2|U3olPV zyIEM}8KNA)4nWYX0N6vTH_aMAD3c6Denn7gK)+hU%K_5GaFU7KO)LFpYpUG#cX^cn zkhL`#uTPcFh*;6LW1uc0uKdGs6Z%X4$@L?Q8s>ic9*P;47}nUo)hMOw4BJ?Guusc) zy6StlA)UPx-$f_EL>4^~z5}xt>#&myVQ0HS_oZGYZYiu|z5s2Fvmlu+fzNrffPaD} z@)auHH4W0UZzT9EF8rM#RO-EI`D86jZp~s}s^WN?*_JBMqYdh%7~opJ>cYyMjzYO+ zK@n~Ee(n8pD0uY)ROnTr`4Yl`D^7(dH1q(!3i6^OOnO3Zs--X6!R+@h9#^^c9$Xac z=A6Gnr+9#`O(KU>3#5R*idp}k9T;08Mtue@Obv=s$fNcSsuFZmMoqSTo%Ri`)31SdqbFQ3G7jmtH1#*YC`XJ~ed3LW)8JBk9YJIWLJnw%$^p%05!KU|Ax-dF5#9rpw70cV zz_PaFy#+`H!|FS>xQ3!)tpAq}N&xKyzIlmv4WXvEx2nqq0x6W| zcSy|>?eX)WX(bu}KiE0ncA(ibaobqO^h7e{5Xk(Qtmms&Jcy2~qH8xne5`z<7 zEh|_dOR=W*Yz95_p^xqN0T9vy#eV*wtDNC~F=&H<{Y1g9{*8md6#-9?#bW=3GEx*% z5Oa#O?HLo#+lr=ofJ8Lh)a!G#x$D~sAn`pOiD^ZPr#NA+hAC*Cj3P0&wB^1rCDa=8`(X#Iw zr9G!4iZd0hhE7`jDsutu^H3p^{jG}nx&#mnd`7Qfvu~b9Ji;A4gsHCk-ue7oa1&ycrypqn$lJLoVy|Mj7U^?N`YWNJbEUC~|Vg z2NNP-b=O!{!`c0p{>g8&CGFK9LHEA|4zNfNt0P)b{f%f??y}rX$}F5!UjjRDvD{BL z*Z4zIeq+AT17$NQL^^)A7T4=JYR#q)x^S9=f zJWT?qGhT(&0_UzW$A{f$>u#F=b{;xxoQo$bSHK3+rlOdYW9c!8&&E&=#Qt(h`jf*E z^~W!mLDRL$?u30JKTH%Wl~5zm-5;#acGhn#l0hVq`V0{@q{W_(P$3Q(2dm__{z;|R z^MPU%O|La&Xumyc79<&@2hURTVl$mudVDqvz3OSy$txCV&ggx`EFEM4LvKwi+1A|8 zr)E>fLk2{)EmOe)esr?$=IMlAH!Ji(t^@zOF%axzNoiB`v7y9{JDu=D)G7Mbtx0)5 zfUa_<@lTbdmm1pWMDL>{R(K&I?tK*BIDqT8gG_o-I#zFr6i)%sM-=0cg=lr{<_R<0 zP#@|vFhZ7IV(1?H9iyxI<118}v&gOok%Y_mdQ%{3ykB5)n9!l=Oq7Sb+kXAM^j+eN z(d}XvAi-T19KNzY;2nDbt;q9C4M?d0mdN{ei77xO5+Z1Xi+mWI`A`ywj~`n{`&MSh(9((7MYk5N)_bc-vP1!3)+gz6JuV&H+{#c8Ib`dvkbW_? zfl;0Hk1?9F^HkpZ_JM8vG!Y}-e>7c0$e`(1S53Aq#I+jQRO-1|$20+smMtbbw~EQU zCPIZLppEs)`BJe#3G_|D<7Z6iAl@W@NP+LiL1{Oe08e+5?Wrh2boZpuD`lYu%q!?v zYHdt7Kwx}7DHs0|GIk9ct6w~rO*PjqQ9?^iKKkuC`?z~2oXXGH0ApTM<_==0K!z|> z4#7kRkpM-3FS($kxJb|J_gE4sg=>9VzXiqtl{{8`USL)7{>Fu=+i|jy7367_t>y;m z<8XbNo4cs?UGBuKJ1%q#EuW^jYh%;CR9ReqR?b}=jE&U_?=Kn;sZ@>ZI43$}VC)Fn{ z%7mwE5#;mRMsG*0nsrYyZ)XQcL7m;PrqO>{dyb3~?PZ`z!~jqB*H~QZKoc%+J`$tc z-TsbA79gQg?&N2!s2*P4i|TaxHkVCQ&iMQJGFK)7!1V#mp``s4icnhce|;&*b;IJ- z4wRS1V?u*95wBst=D91R5s-6J12g{SX04;k?0h4xl+Ox@U5K#5ljFnSZePm%Xs!g= zj&5YKp0kdw=7xPj&;tU2sCS8mb9A1V;7Jr>YOX4a-xC#v@2A7cpj`;dsZ;R*Fvr7VaELGrS)z61TYw-HS z55@OR8tB)`+UDHy zJ}Q8B$hfIt(*gX%NiaknG?~1Y_lRJ(4}`DDFczTT6*WJz=(gCv(I;&{ag}a^Ns}j2 z!u%(>6@U7x5jjG4#sI`HEXDw67H;tsZ;EWoc2Z-kh1!V+a<{i+QpXcjL=Cmhii8Yr z9`XGEmgwEkdGhZZzQv^sGpD@w_}xS=5I-K|o}GC;$X)P*rT}%igny+{pp_2I2{c@U zpgDou3ixb(SHl125Kd5pvnT$z;L!LeH#hxXG1tVhb-HJg_LC%u$ zA@iLx?+{?x1lpRUuz7#G=}w&VStWG+)#Bl~-`e?}AIhg2IZV%17m>fEw%7{btqSmU z{pO<8yjQnL&H&)~SM!XB_zKf45CGGFvn`$pi3?tcyK`m+@h~r+!B?D|pSolZeVwD4 zb2hJj@nL1^8Mv~n{Vn<)0Cz?^p2>KJV;Q$|a14O9 z4%pM@lWen!K=c3Wu@J!#i9>D1d&pm=X8ceg5zVasU$1d~?i>wWePfYrYjn2I!1=_Z zGeZ}NMLNwM)NO|%_upm1t?*cYXEs$nYl*Abln!;KwyVMJV5`-6f*1zc(8 0) + assert(bucket_acl.all? { |g| g.respond_to?(:permission)}) + assert(bucket_acl.all? { |g| g.respond_to?(:grantee)}) + assert(bucket_acl.all? { |g| g.grantee.respond_to?(:type)}) + end + + def test_property_bucket_acl_public + bucket_acl = AwsS3Bucket.new('public').bucket_acl + + public_grants = bucket_acl.select do |g| + g.grantee.type == 'Group' && g.grantee.uri =~ /AllUsers/ + end + refute_empty(public_grants) + end + + def test_property_bucket_acl_private + bucket_acl = AwsS3Bucket.new('private').bucket_acl + + public_grants = bucket_acl.select do |g| + g.grantee.type == 'Group' && g.grantee.uri =~ /AllUsers/ + end + assert_empty(public_grants) + + auth_users_grants = bucket_acl.select do |g| + g.grantee.type == 'Group' && g.grantee.uri =~ /AuthenticatedUsers/ + end + assert_empty(auth_users_grants) + end + + def test_property_bucket_acl_auth_users + bucket_acl = AwsS3Bucket.new('auth-users').bucket_acl + + public_grants = bucket_acl.select do |g| + g.grantee.type == 'Group' && g.grantee.uri =~ /AllUsers/ + end + assert_empty(public_grants) + + auth_users_grants = bucket_acl.select do |g| + g.grantee.type == 'Group' && g.grantee.uri =~ /AuthenticatedUsers/ + end + refute_empty(auth_users_grants) + end + + #---------------------- bucket_policy -------------------------------# + def test_property_bucket_policy_structure + bucket_policy = AwsS3Bucket.new('public').bucket_policy + assert_kind_of(Array, bucket_policy) + assert_kind_of(OpenStruct, bucket_policy.first) + [:effect, :principal, :action, :resource].each do |field| + assert_respond_to(bucket_policy.first, field) + end + end + + def test_property_bucket_policy_public + bucket_policy = AwsS3Bucket.new('public').bucket_policy + allow_all = bucket_policy.select { |s| s.effect == 'Allow' && s.principal == '*' } + assert_equal(1, allow_all.count) + end + + def test_property_bucket_policy_private + bucket_policy = AwsS3Bucket.new('private').bucket_policy + allow_all = bucket_policy.select { |s| s.effect == 'Allow' && s.principal == '*' } + assert_equal(0, allow_all.count) + end + + def test_property_bucket_policy_auth + bucket_policy = AwsS3Bucket.new('auth').bucket_policy + assert_empty(bucket_policy) + end + +end + +#=============================================================================# +# Test Matchers +#=============================================================================# + +class AwsS3BucketPropertiesTest < Minitest::Test + def setup + AwsS3Bucket::BackendFactory.select(AwsMSBSB::Basic) + end + + def test_be_public_public_acl + assert(AwsS3Bucket.new('public').public?) + end + def test_be_public_auth_acl + assert(AwsS3Bucket.new('auth-users').public?) + end + def test_be_public_private_acl + refute(AwsS3Bucket.new('private').public?) + end + def test_be_public_public_acl + assert(AwsS3Bucket.new('public').public?) + end + +end + +#=============================================================================# +# Test Fixtures +#=============================================================================# + +module AwsMSBSB + class Basic < AwsS3Bucket::Backend + def get_bucket_acl(query) + owner_full_control = OpenStruct.new({ + grantee: OpenStruct.new({ + type: 'CanonicalUser', + }), + permission: 'FULL_CONTROL', + }) + + buckets = { + 'public' => OpenStruct.new({ + :grants => [ + owner_full_control, + OpenStruct.new({ + grantee: OpenStruct.new({ + type: 'Group', + uri: 'http://acs.amazonaws.com/groups/global/AllUsers' + }), + permission: 'READ', + }), + ] + }), + 'auth-users' => OpenStruct.new({ + :grants => [ + owner_full_control, + OpenStruct.new({ + grantee: OpenStruct.new({ + type: 'Group', + uri: 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers' + }), + permission: 'READ', + }), + ] + }), + 'private' => OpenStruct.new({ :grants => [ owner_full_control ] }), + 'private-acl-public-policy' => OpenStruct.new({ :grants => [ owner_full_control ] }), + } + buckets[query[:bucket]] + end + + def get_bucket_location(query) + buckets = { + 'public' => OpenStruct.new({ location_constraint: 'us-east-2' }), + 'private' => OpenStruct.new({ location_constraint: 'EU' }), + 'auth-users' => OpenStruct.new({ location_constraint: 'ap-southeast-1' }), + 'private-acl-public-policy' => OpenStruct.new({ location_constraint: 'ap-southeast-2' }), + } + unless buckets.key?(query[:bucket]) + raise Aws::S3::Errors::NoSuchBucket.new(nil, nil) + end + buckets[query[:bucket]] + end + + def get_bucket_policy(query) + buckets = { + 'public' => OpenStruct.new({ + policy: StringIO.new(<<'EOP') +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AllowGetObject", + "Effect": "Allow", + "Principal": "*", + "Action": "s3:GetObject", + "Resource": "arn:aws:s3:::public/*" + } + ] +} +EOP + }), + 'private' => OpenStruct.new({ + policy: StringIO.new(<<'EOP') +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "DenyGetObject", + "Effect": "Deny", + "Principal": "*", + "Action": "s3:GetObject", + "Resource": "arn:aws:s3:::private/*" + } + ] +} +EOP + }), + 'private-acl-public-policy' => OpenStruct.new({ + policy: StringIO.new(<<'EOP') +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AllowGetObject", + "Effect": "Allow", + "Principal": "*", + "Action": "s3:GetObject", + "Resource": "arn:aws:s3:::private-acl-public-policy/*" + } + ] +} +EOP + }), + # No policies for auth bucket + } + unless buckets.key?(query[:bucket]) + raise Aws::S3::Errors::NoSuchBucketPolicy.new(nil, nil) + end + buckets[query[:bucket]] + end + end +end diff --git a/test/unit/resources/aws_vpc.notes b/test/unit/resources/aws_vpc.notes new file mode 100644 index 000000000..5112797bf --- /dev/null +++ b/test/unit/resources/aws_vpc.notes @@ -0,0 +1,91 @@ +#=============================================================================# +# Search / Recall +#=============================================================================# +class AwsVpcRecallTest < Minitest::Test + def setup + AwsVpc::BackendFactory.select(MAVSB::Three) + end + + def test_search_miss_is_not_an_exception + user = AwsVpc.new('vpc-87654321') + refute user.exists? + end + + def test_search_hit_via_scalar_works + user = AwsVpc.new('') + assert user.exists? + assert_equal('erin', user.username) + end + + def test_search_hit_via_hash_works + user = AwsVpc.new(username: 'erin') + assert user.exists? + assert_equal('erin', user.username) + end +end + +#=============================================================================# +# Properties +#=============================================================================# + +class AwsVpcPropertiesTest < Minitest::Test + def setup + AwsVpc::BackendFactory.select(MAVSB::Three) + end + + #-----------------------------------------------------# + # username property + #-----------------------------------------------------# + def test_property_username_correct_on_hit + user = AwsVpc.new(username: 'erin') + assert_equal('erin', user.username) + end + + #-----------------------------------------------------# + # has_console_password property and predicate + #-----------------------------------------------------# + def test_property_password_positive + user = AwsVpc.new(username: 'erin') + assert_equal(true, user.has_console_password) + assert_equal(true, user.has_console_password?) + end + + def test_property_password_negative + user = AwsVpc.new(username: 'leslie') + assert_equal(false, user.has_console_password) + assert_equal(false, user.has_console_password?) + end + + #-----------------------------------------------------# + # has_mfa_enabled property and predicate + #-----------------------------------------------------# + def test_property_mfa_positive + user = AwsVpc.new(username: 'erin') + assert_equal(true, user.has_mfa_enabled) + assert_equal(true, user.has_mfa_enabled?) + end + + def test_property_mfa_negative + user = AwsVpc.new(username: 'leslie') + assert_equal(false, user.has_mfa_enabled) + assert_equal(false, user.has_mfa_enabled?) + end + + #-----------------------------------------------------# + # access_keys property + #-----------------------------------------------------# + def test_property_access_keys_positive + keys = AwsVpc.new(username: 'erin').access_keys + assert_kind_of(Array, keys) + assert_equal(keys.length, 2) + # We don't currently promise that the results + # will be Inspec resource objects. + # assert_kind_of(AwsIamAccessKey, keys.first) + end + + def test_property_access_keys_negative + keys = AwsVpc.new(username: 'leslie').access_keys + assert_kind_of(Array, keys) + assert(keys.empty?) + end +end