From ff0ac27723634b43e4dc33e493f595bc78f4c131 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Mart=C3=ADn?= Date: Thu, 5 Dec 2024 11:01:42 +0100 Subject: [PATCH 01/67] docs(index): update index with images (#6015) --- docs/img/compliance.png | Bin 0 -> 280228 bytes docs/index.md | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 docs/img/compliance.png diff --git a/docs/img/compliance.png b/docs/img/compliance.png new file mode 100644 index 0000000000000000000000000000000000000000..b08fcc365123a402c40bcb92ea23ead5145fa5c9 GIT binary patch literal 280228 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⪖^_3=;#~LH9j*pYN*r9D(}^YGs`>r4 zAh*fww=?nxHaVMxQX&t*8iVL7yd*mvuXIgCg}t`4Nu};p_-BTRtF4jv|A()y0E#kf z+a(qSQBpyWZjkNuOS-#RNF)0C?(XhJYFWDByx@0c&iQ8ke}>r^nB7_4 zC+@ti>lQIpXi-ddJ+ZlV9#ppd3q?<33_}6=fHy)_h7nIL-wIHx3#3tN<7^ zWd7G=XtSVo%JU5#-H`H*9ESglwHCzm@^nP>!2IQk*-dtki-V45G>G04;G6mW`CfZ} z1>9DuCR6pZ+mH(Y?)Bu_pxW~jjUq120_x^XZ{D|+X#O?xt)u?3DcT~4>~Xl2$?AD( z_?w)BS*ZEiGF%=DQKjXZ&#Dl@=>UK&1~q$X3U7(O+gxr?|9CpUA)#Pjc_82HPkGO* zc)i_J7=iME`x~IwrE5ack$D#O30@!4=vQt%CYQ9moq#MBBYl{qANXCofL$5|?@lIN zQI-15g44mYn8aI#>>BtRzK=T${$c=&H%q4tj(KzGkL_0fJF)ztB%WnTHdBQN)URL~ zY(*_!xa-qCiT=v-enqn?!8-YX+szD_Zj*^|PEV(VXt~Gg`FP}$LG4n+G3hkvBHZ!q zVZ;2wI}8GJ>H?*bDZ|vXszg!|6c=!7P`O?YJ+*v&W^yigr)jKyG24k%f&bQLYqkvY zWWdVvMl$tqQiVVt56<=L3R6)E2ZQ?!_h5q&9?Svt)8)++hWpeOE}(5`(6p7lmssDU z9olCUg1QS0Xrl*ifxa^w9wGCcj2Fza6`+F+LA5Mn41YRu%)C!EEq?#^n;R!hO1!L& zfM?P{5{>xy9&Xv_<}3_Vq_L=GHUQ>5q0HTb;YhMnxvwvk_o3%}PKj9)V^X8zJ;jVM zSR>08A5UR8zpoB7Bj~78%M>qJL6pzbQ!^%qTTrvNf^sBeM3=2`;*w`u5 z!p-)9X({#%xEzHg1FhJ;CC2;H7Ca7~r77gBKm+2~+bzpsARF$}MRd`E!y^3Q!d}sJ zzG6i@?|4h>I`L_t-JiT0!*03xlOsaZfScjq&(!jS{|Zb#ABZ#t z^lN<^FLXdDXmD<(+jW3UV14c*3Vu0uT>AYm{(_lj1L;>!3P5*l+9jUDf(#%s|Jj!M zFfgb(Gsr}#q@$ii_qy%$64P_cM<(;fYR!@SFt~CD6=j%KlfWKP=9C837Er`+;87}6 z(j`U%??fIUfg=2&Dnvc?)s<3d@*x~sm zFm&VOp;so-(KcAWKT<6DXh8y~J?Uocr-Ir>SqU!MdX#vGHk*s2rQ*0PvZEkpaV#gx zFH`>NL4~i(hI#3%wW`1WUL4EizehHsxjT6UyKv_L(cmJJN2>xiwMY5=vu0?BuP_!D z82mMDDqmc>qiWufkoLTM$?x$TO4Sp$9uqHVh&w(Hs%{hq)>7G~obhQbML9s0Kr0df z@l)Qn0OfqIYAbOc^cx}3*ThEy_b>P9PV`ocAF?xPGr5XbCD3&+<|5GYD?wKE)HMf- zRWlejB#%CxF{Z=@?+oC$$$?=+h{|b)rtMk=^f#Si0Yr11b2Vt?(=A2O50P~8Y6-#* zn-#~dX5;t*9^)9**_B0R)0UBdx{#IWCQ&E8^GhYAT#1J|41Gg{+>dLi}B$LUIC2{i|$7eJ*vbztxMiu9yEXoO1Y@A}F9 zrG>D?GmE4b}tOUQY0<@6?#fpd6_$v^)IO#i5>NpNctsAAV) z^|*vAlz`9Rw7$o;eEl25DvBC0{PV}?Oz|!<%V=PfcjJXMfb6BqrBlwXDP(g=b4XxgEueQ?OE*CJH4;5!GhB7#xMse+YIZO1 zuH?;cpFt0m*p5)fbrRB&^LNdE0vdUQxR+O?<8q2~-3eDpSUs#_{t$vgGGCKfCxOX)mK8-~8 zJ_Eu&;hWl!1B55b>YM6HVppwag9upFrWw7>!Tu7iC-Pi+#D0r)6>iPc>-%Llow7uRJ=@Z zcT{?@qn_D26y0r^%*!D*MuLJlDIHjHN|a^chj!N)C@+_xrbw6ia5Sp>No%Im;+7tN zsMt!PDA>c?qCFyPK9i7+k?4SN}^sR&ikknrmJPj}d?KL5B!gBg2;RttO%$%x{!g z0c|?YGJDP)ddYh`e}$&1a#jToGug{%3m!6wXj-|7o1*ZK%FQ(2lC&z?B9H5h%8QKsgK{_Wo8ypd91BxsSRM=KGJIVNcmVFe~&eUBZkVIi1IJ4aM=fx0S5S-sxYq3H7lL{m~)b})!41leK-G{Y^xuUQ(>bk7AQ@t z^`KRE+*L6l;TMB?Uz4rAfs?NJ+`!$;@yZD2=`;z#naH#{TJ`X)%uWBApjw?&%3-Ny zPQ*9{LcOPG&l+hKa)?I=cgbA7_4NK#2f&LrhD0L#OH+x$Hm|~}`Ch-5A9)cWeAUp3 z$8DiIjw7Ubb@HXPxd7-}P@iK5S0XYd-Cek4Y^J>=3^P@<8fzEp>;RpnlMfi&1aomJ z_{^#_v1vcjYh|KItfB+Rx@nCQubDSy`BY^c7QSPF-a3@ z%xiwx(4CwzG)Ggq@zT8OkBI^%LYfk@sVwPE-1vRt0rI2xd=H1i6EAvXwJseW_WG00 zJ}Xh(4g^~~b;&2Xi>V&?Nk+%x?`-!ff8>O$Vn3fS;E?^VKl+Stak%UzAB;G9KJ?xP zU@`JP$v86jwxD5kuvVhOHQd~tSB@}Ny~Z3GHJ37}6+`-{JNJn^)-B4 zW9ScQ0M<{fI@>NybgdwX;-X{)(9k9j!Su&f7&$Cp4kRcu`R(px-dK$!h!tg1g$bn4 zTsGEYA%Te&CTv#(kaT2Q0hQppOq=5%;>oXcTtBVk6W^Ysi#Z7FQY8SzDqwF@5fgbG zHNZUfU>4o|!i$8kr}$a&U;Mlq21LyE&a!jl&xRZhh~BikdP$zo2tJ0WySu1mdS4AA zob(0jw0`SIN!@6O_>fqWbBAR46Nu;D=)c*OJ|Mk`TOp-_w$-^d%Xp$|y zX=~tI)cVj;;cr5!;8$R8K%B4{YadpZbWZ>Io8lB0f1ge)J=@q?kK#eGr-%2Je##HD zKUF_x`tHJjZ?~!~S9tnTz_*9vQn|16*;8Km;TXlbHdEZu6@hQsCo4JxN{`aYUjPPM zkX*;`a)|xgZJXo2LLZq-E3a>Yg&fyltxmOA))v?E|L@&Oq0@~}A(hWzsKL09Ievoo ze0BtNFc~*|<|w8@?cp$nDkhpOk-hxYr(wU^r^nkX2UZqFv25ZVSnZib~2dx(>2hdi>%sb@7K0_ z_)}ptLe3fb6Qe7rzjy6Nk)7voENO&ioFRS!{=`T@KZjdN^F2B9c$4QvYN{mj#RMu@ z=*fo1?zqA3xxIkq=cDAM%r6z)*_=(kfB%lSfB6s*RWQ?MKGofuOO*MrQ<{ zm6g7ToQbI;(m=~o1!mCVy+xg2+m-e1IC;&;)kUSzNZ!uIjy9%z_Pq-bu1z#RLT~rn zuAPo;bN;$POsSo!Ea?HI;BY|LwjaRYPBuA~1V3(5-XB0BV_!a9K0!Es;%`N!?{?>} zJ@Nnt@s=tKka&6>0Sx)*uM{xqYyUEkxM$BniH?s?3WVU{L+XybG6FJEi~)L{$<^47 zOb6lS3L}GdI}GEO9(Kc;_?$7}E|l^wo~!d=(I6j8HnW)MqVG%pzRW{xwVJz%%xrp; zY(c3^NVNpXgZTYGIC)SpYP+JZdcHy=FoJL&e2cvf~@u{?*8&9XreGdp34FqsX(AYVlGmr^yUYC?ZlHn#J#HK48DngnTzBeVcag6b0h+>)KQdig`w z>J?~~LSOMPZFOdJmHitKS1QXLUhW(d=Q-w_*Cp23B6g#Kf*pW-Zvr`OvC8ajR#vW5 z#{*15M%Jkr{vl)_nyr8yKEl@I9gQL_B^ooy9p905;P2l}YuRKMAG&L2cD>yU&g0*l z97w<`YBrB8)-BuQdo*!}t0NWs!lHBUcx{;pLo3;up6z~pviE(wo{wW3d;6T;yYr#? zpi0LbQ5l(jiEm?JBI>6$rwOoA3<;cWt~20BD^QDb&DFuint1QGVQSU9@GgWDB|1nL zpe-l=)$_z}!iC_U*KiJnNhW;J^iaO&=46&5M4U_KZbxh~yUNa2b2?t|W2=9vfLrRj z9Y$>mhsmU1vTJ#R7ZCcPSoEX&U?a?oo`v{J%U+*s z{{cn^aCOxY0Cg8BZl}f(HZ|lp;!Y`g-vE)n?DJLrvPYRm-4roXMWo>=uOMx$?u3!>>%#cYXcz)1MW5AEbz3H>TliW90ddV29~zf z=G_e-fhN1YUW`P`NO^kv*J+WxH5mKC&!x6!cMC*4*|a)M-%P)T(e_{*x#vTp8s~595WzRe&`Tx4$7`oNjcq8&hz;XN8&(uZ^BY1 zjgY+#t6{h3V05Zb_(u_e1a_u{ZifHNy|Lh(foFFmiF|E@1|QXSosp~R5ds>_Yr^Y1 zNo9H+9X=R}(-nMf!Te=8)tM{Fqn@bk*)x!#u>33@1<<5<@~*xxg*r=yZq@NOJ=)BM zi!<}MSlb%Od*`uOt0X*?wr2Tf)&}2EFOK21<5K=EE9QK`HAddJ*1uRmK2X)KDNmj_ z3~c767p-HsKI?GGda$TEKVEXG4pZ%Ci{YEy7qwjOOAft#-b26$<{c+QU#W6D-vw>B z*jPvjt1Yxt^J?42l?0*SpykFj{f-H&tc-??=Po~lVgv@9B+K><2xiA6BD?-;u`=t*t#6< z)L1S6K&m2)$~UkcUPgoU6p7V2Qhofa&8v5&D2qWnl~>E6jEh#XX}^3XlC&E_2J-k4C1}v1wVt z(28OvilmkI&~%Jk9?4JwE?b3LH}>rK`I?3SHL*t1Ay>^3Na~Fbv_)#)aK4bx_XHYI zYk2sT4T-ZmezOC4Z85IJIPHFL?(__qrm~AAkDc_sN3S{D6N{edOZ}1ulcBG#Z>|VK zmFqPy@5axt%`imD3H^mH1Z+4%2ExdubF-3*GHU)Uc>81GS2APkMnqPM?3e_af) z5%_vt%W(b;4&Z}%AP~bt+9r(d=%{$IV7Ki9-heeiCvL=44;Gj(Y7f-kGb zuozYa*f}{X66N)-A^|PM2+l)+GaWaR6^Lb3Z+307vOo1u-8C}Sz->mi3Y;lV0HMc? zV!dOXww-hBEla$r?F>Aohc>m{w92YM``JqK%;l6UVIC8)hooQK{*0E3w5dYF7sgL7 zKaG~@QBGFjrgf)iQkea!UXe*mXM51W!Xi|`Vv29=JHIaCxXCk0_*$P_vvNCXlKB2% zc;D9TzW!!0%t=ISiJJ{0SU%UR)9uF@S8xhcro~_Z1?T=4JNihrwzjpZtG4{^&$3dA zoNLqJjDEZ;|NdfG(&liIMCcSio{YkdGU>{cG|~Hhaqom5v_);+C{TFZ*4a0<6k%*)jbm1#<-Vg0(?&kj zNgg9eMOY{@6KQHEaQRaW2waWlbu?Y*_d)d1$3evzR_CN%#hpaVfA^X61;FUBnxQqosH{k_mCy-_oZpXnlCPAN zBNj;mP_ZPxk`}5|DSe~lPvWku*voTLr=yl_%nb>kV6OZiS=%5bfuAB3H{dVR5&6RD zj+pWKE@I8qq4*dXiDeP`n|FUi?1sCeP)k8SvKVuw9ng4m8nF!jXe14`>?!6`Lo>gD zPeT%d-&%og$Q~JE=3L#c$_pXc>Et6lfYBIXEl<`i4cY@p{rg$K1Tzlbeel~shzUnc zIaPN|Tk1(KO^C0)Jbrs2>Df_3*8fTK>UYZ{>* zs|X6?h>qHvbpjtZv8T%I92z%CtQ%S(-)rYBRXzd|ItXd)PScsY70%V(Y2O1LBa)#| z0hZJmTzaTTlSLhem(HA~=x(g;NqUNkC;}G1hO8EvHHYL;B+aGYmxg%XCf3EQCnM2q z3PkBlgLne4Q{3cMto{(FHB#3RARuFnS}<+tCTJoVT7!c}fSF1*QaVa2(2?{ze7$w( zEmF$g+Xg;Q@ryr%uzDWm=E2J{VkyeST1NMdu2gT{Vvg=$4aNVpyBgA!jn)0p9BX4b zeyJB#Qh(z*_V#2Qk5#T&{vL(}6zueqp|G0-leca;TkDS9jqkXWVE-FRx2a^j^!4sX z`N9P{td2BP#QT@cq)b}SnuBVRui;)XhzcdHLFXL)U$`M4I5d#`-*MsiN?=@Q@Xesw z!CDqY>HFt=5B5u{@t*{?m$bPZLgG%a$fIgCha@A-SM`a!F7uk-6EUcM#3p(HSOa#XNnqJP-j?qpWE)06ljUlZ*=beT@Y^(qR&z)4_8A*NVv^{ zaclS6w|)JuPl)(-e}f+Jxzno>px;o}uqk+SEsgdFSq$#G>0Hb4+QmVVxy68#(ye#r zXG#j@h*wz>(ck8o9nLnTqzrCew(DVf$hWg$>In5--SL693#VIeoW__3Xty^)`7^7s zC0#4JHn+^IUPqvcydIOiT%3}FGQrO0gywP+$4b%)Qo*zl`R2xEmZ&%QOaE%7S1;o( zGZ*X5U|8`u|HEeDaMvkDm+A*yji!0$pe5NV?1mN52jr!b7Olvrp!cGvbjoh6`V5j< z!bIj2)&Nl;Zu7IU?cFKOL_~(b`+Uk>VzPk1Op664Q^VNR07h82pkZ<0-I+X=%2X^1 zAF&)|ZBKQM5`$9;*PuJSC{-)FLehRQ_Q?5R%yuJ2I#=5O`!eao!dZ{n3zu{5c-Z- zik^yp*YZKhBRDCn-L>|9IP7Ic-ykP8Bv3R z)Yw~u86Lb=(RFW!m&$=|yvamUPYr82tgcu*vB4YM| zGV40%mV2*GTa`($v;n^rd~zMLGKyPv@rnj%H@mjH(D$5gh26uBTPBHHfSi6g(9%4z?;A9OdtM!w_&O8vxDexV$l#AJ?q zsdR7X*11h-*@BX>7&OxgSw8O?z1mF5g)D5x-!(Yb4JNY5lwK7yjq7~9hL2JVa(Sd3 zxA^QjgO)kIItK^qV@R9HyoG;v6y~9HoH;+qz)qZgC*Hs;#xvJw#p(IN^sMe|o2@4k;qqoRb z!oxAoj8+Z6Pk-k`94Va*_6A{)hAm#{j5uj8@SL~vyWw59@t>C){O&qWoY4Mn5`}1M4LG*hY#ZB_ zMT*O^u3rz^^7&PZa6r+QL=`lE{7mo>2~@MKifTvol;(2Rn4Y_utsIbX9O;h#gTy_2 z_VUs8tj9HfhKYd?VI>oVg!|bvnr*)zd+bjYWy$Em*+3Ua&rAoJZc$9>Fj%;P=|C-C zq5eqwImzIu_}Z?{t;g(pGjz<@%QTodQLFfE~#2-9= zzOizOg)PUJb7V}r%Q?CBL(+5evcid#@?Yi1gK6_=UA!p9E;)Szb3WIkX+Hf?Ag99hqgtb#4Nxjw~s; z9?&`y0uQ~y!!NwCmvEIO@umL?|xu!lu~{vrNuJWY;@$o<+R-7 z&q{G3=@o{b-C2gXX0jD=h)kXsz(wKIX8f1i!|k5V^V29=!o%&Q>-Qz&FCw2mt9{WK zZwBY|oF9v39oHByvx50j*|u;;Qy9A4Q~lmtx&57}X)(hfhyXB9PnS!NFC|KZ(Bm5t zlnH{ ztxy2fSU|Z8)`ER)X^qfqjV)xw)hIIf+BlXUL6>>(d=8#7?hF4&goXESn^PN>$ct^F@ve)5HrVeq!zDQ8YfKW{!3FHWm~t6tZot)`@+K z5;QDXipO;oTE8w?#>?NM^;`H@5)lN^LR#+A$2|Nc-Kwzz21MSOh-4i6v#0Zq(TB&X z<-TMeufDiuWLb?AC~f}jAUVc0bI*F;lHK=RKaiv9? zQr_8XN09fDxRoY2p1lli`}HE?Js3luG@p?B z)*>y?fMQGPMeG+=-HC0B=uyn(zd9c8g$%FW_%hCDIy+s7xQ62w$f2jgj&k^Ylseqf zg~P_piWQZy{JxSr;?NQm@5R>U(EeVncXYdJQ*BBeUkG^;!9t-^cZ>E8CSj^dW$YE5 zwg!vjwr~bAEJZP9!dni9hK2d-g?mcjfe(aEu-VxhR&&KwO;!c!@-ov^CQFhg{Z&=r zp+Jq!Y{kLoZbl#v0rbY6l_KxhoBAk}|9%|fMeTN*)kv&yyVZ!tAu(0AFE?vFTIv?x z;fSgd`Yo!~gsTrV)sq(G8`x?yFBfZ4-`G5N86d0IOpqSRk_=AxmgbJLmB3l@)B^qt zSQS_)qHo~j%3mGFVSp(tm<8fww^4^LeV0DKoO zsIL&Y=&-`3?Qqc@-zSL~Pzh-V_3Va=Wlh!@Rbv&d%r4yHPHydYubq`8pgZ{v zNRRdxfzVEhEW-V1RrWI{W8=Dl)4*khT`@;V;|rg-y3aRT@t5aq9SDZry4~oQ-B29I zjJDYCU%8Mdx+VwE_5^u!#c^P9^IxJ6j5EkEN-Rnsd83a8D8Fo`L*9{|5?TXFW@@+? z`|Rf9CgC^YAuXB&8JH*j%BGZHnZwNrNoAnup4RRMxC8;Q(Fq?$;4q+|6<{Z%Vm;Kjg}|BFAF9zT9NFKXbmCnw90o!76{p zpfrhb=Q?g=yweWitduV8HG=Lls4L1ycfRu$8*Q3F9)H|2dTQ5Jg}u|`Nj7^(z7ATP z$!k#8c1qT=lELEMY_}XNzogwcZ{^aovyC-JFN~KUt&lQL-l8PuCY-Jf%q_H>ezs}k!6Kvx~!j5I_ z5hH!xH#Dr81O!N|yh^!FeR2t>-G%wDy|zS!2WMo)+7>JMSFbL<{Iq6+8?Z;Dj7p$V z{;fA79-eS%Ej6O|6vRMZ!o9t_=}^>pK9)rI=w4x%5oEE@jt=*M27*fW8Qt$$T1)fN z{HsJd9b^W57+sm)(%_^cVlC9zB*I@4j6W5lN*dV}9^&pBrU|+~lieNP-|eSxA7!vl zbcwxF(l%+Lc*ofHP>%6hm}NRCrJ?ggNd2Xdvek>&9^X(6P;8n1oCId7qFJRy^_--~ zL$>?h=6s4!^wgwZrG|$uz`u3aG1PLPCT8uI0@6$kftC>)s1}^YrzEXAq;~=d-?HQo z4$WpQn_X^0H0MK!DZ0l9Fb#*Udk*TC)}1DRdVJMN&FWy=ysu%*%cB{eBSRD5;JL!Z1cM3`6?D7(hPO*Mva&0U8K@m7_q1Ke{m%*|CY{ZWX=mC+k zOnpyJ?60|7Yzc39Ttc+F`cj2VP)T^>EYQ{yLMUT1h8$Jm8Y`of?fV|$-Wl)1i_b!o z+1c~~cVyH*ndebkK9YCro)`%H1clqLVvcV9%1h5Dl~R=%9(Ley#5iwv?WKUkDLL#bkBM;p{eS3hahQ)ZWj+K#Rn!9DEbzLn9PWdWZuv{hO2!O$OnzXvKqMgh zwl6lQP5FDq!+`kpL8?2!JlLAt?^OY302ms`mUpwr!e_8Y=xH((70<5iJ8J1LRq4YZ zB6*`TUN?4F!zI<-FkSb3N^OQJ1z*_f8YoI*M25yB?e_1Ka?O;xU>>Htz%5gZTrn_4 zi1R|S#>}RXPrui4pJyKUd1ck|sG9QNu=hy&82+=he_=|TDsepQ{bO9kwpmLP)lMEI zp}b*;U2l5VK-Q=G`hL_hVQ0qMBZO&F^~_s`J^QF?9!CeSY$*0I|2wHfFtYsGgy*f+ zqX+I}2`NJ}v#1G*XVE^Dc5x+yK?$t^`2&YV#DXG$Wa{J1gtNh(RXlswlGwGDFBAvX z1DW{D$>?NCGp^W1Gm>xwFMp+2x3lz9bjkr`vb*eXB9DlLwz4R%bPVXu08o^&#DqGb zXhT$d^;vm`xU|jJJXV8%n*E~7*c`p?m3&RZvEkCR z5)&twSiFV|8}LBAf72hRt{JEWGs!3@2z9dMA50-G8$7lh~D51u3iO)_p~5dc7zh(reyW zm4~f!3x-065S$r-g$t;*%Euh-A3RSKuZG8x@qN zlR68ujM&A2Ze`AnUTLdGHz6iMah+1J8fDj1h3m$`zUt8y+A@mEjBa3;k}qqnvdBtF zS!~YND)mhOjH_x#pkjZs{_)3QpOvvdnG0$|SLBC$uec|?0H9S01h^!USYQ5y6-3bX z0jvH>>J{Ua?ASpzHF|$kU*p@$#~f5`k8`@Xn*oj+ZvxCJzWN3ZVv7@s>aI*0-=NWD zUo@w%c*c9DW9ETHCRW2#{4HESVTl%%ikPLHRFj8-`pRKwe?ZST$x(SbM-v{@0u|sn zTN-1&`WP%!2W-5-1$0BEh1#MB=m%73_7gJsG6y06<{T#% zKnchZtvZ}jze%GjZfbz`dl54mj4S&nwmFZnHgijzLN9Aj@!(zX9-~>eYwVzwG#0RaX1XuK-=M zRyd|gR4fu`B5VQ1#-vbM&do)PRgY(359^gD2mh*65_CTiBdtVC!1*sC&h75)4OBO) z=>Eq^mOCH#!EIS+z=-}83&m?GzT$;T5io^R!eTL{a6{e>ru)I#2u{Uez;tuY%tHU) zHR<|CpeZsy2j`8h=xDN6YseFi74&@(jRd*N{VLKEJK=gepKzqs1CV?oiZ!`adSa~|u$wF5 zSf3(zyKw!Rz)nZIh~YSVk>Y!&nC^($SoKxtJ;-3=(loc{j~Se6`xqlq4BV zp@t>{++PiVenhAl)L6a40K}0p3} z^;R1mwV&x~T^eUjE#-Jb;+R^wrX3>@J!N`=citt;0duu7xBSZti^hMlAS*K>8NpTV z=(M#>#T>=4>}<$uBLP>DQM?6>eFgxn-XN{I*Drp4k7 zApKtmTL^p$kse0wx8}n5-ua!ENiFOqd4{?hO*Y)?uijA~ygZa3l%i6uu4F$&mj4sT z=bn(#A7R*++-ZI5Q$ssta@}fxn92Rri zWTH}q1d<+YMevZU(*9KwAZXegj*?H94B%2ymeN8zGgD|#4E^1Qq{e5F!*~DtVopox z`z@5q^}7FE6C-sQD`cIoX&KVrD{2GK6vn32Tru6GPsrRS zBFUbHtcrI*u_U`5({@^ihT%e%OOFL08rW--dDo0eY_)T$l(kch|GPa4-yOj)pnKPDrP=8Kin<)UY z(2TDt|C5gcQFwzkexx9J!1wnYv4CBlczi|Lt%-HWpp;CZgy|s!LQJ{vp3JR(IPB-N z_1`!CPp@#^g4XHtZ#<@55v>k|R`x9j`l*$xgM;r+687nQuK)XEL|vfi;sr97)5XjQ zpqQ7*Nr?FmS@Kl-`G0@BC_z3O_-d}D>Zn) zy%b7TZv6qqa*AtcXsB`YY}}kuo!X|QrP;1`iiMss12|$YBM_veq-b#}I}3neY5Oo| zL2wghYgM?wH^GC+EZPsT{1X-aENst@>?QNWXQS4_dgklFKJhJDwYb^;@hzS>0y4+t z)=&OHGzJ9s6}nGnv9wQ3$tsb12EIX})+)?>Ko|VmptnPCt-Jm<@TcSlLd_?SK45{VZ)_YP&M7Am8cH`U{H$Mg<9-5Bi5;Au6+M~oi>r(rcKF&i3C8~D$0QBOw2G7&&>elY6Ac-mVcK7hX!Mo>a? z`^>-O08o$}zghH=o)!|IFYD9G=C6ls^xSL1+aEQ<`-C>Ycpv=0U+5D~I*OU5OAyb) z=-L7#p=_CGX+NE0_KSsa@~`(1EM-dW%9Y!myQ((Z{vMAXA^{9RM1NHO{zm7?H8a0w zbEB<#lA0O}7~0Z7ou4m#h#M}mmB913@;1we1OrMAd*?rr%(pW@V_AEES7+<0(VdHa zBzlNjBR)6YMJ^E?rht8u-iF4S0ZBmKj#Z?>kZLj zcoKG#HF}g^G`Fw9NF*iyu}nq0HsDtX;UFRdFMvu<<|_cq3h)!R0=mV4HGvBe^f_)5 zgB|=4NWpN)>u?zAT6au^BDAt|a}lX-l}Ah5Mtt)F+==sM=|fSqLf*Qq98$RFwL?6u zc9~kY$8`}Nr?Y5GigKLHKavib*EPQJ&9Vkn%L>dV^-+q--~VC2S|9>Mx(|@T=Uur= z0wDgLEkfpmPlqb=4e&3SbR<KqX`d9wR&>D{db#nj zTpWv2nnSbJlG=E&#Rr2MHn+QLgS|UAaI;;{g?=TF5-ebE__xFv@;&dtURg;{tee{kKmv zvHmDSf#=wp1^A73qigctfdp2pmr6PQp6n^$T0uiR=`2jLinrFwQc|FZK_@zEa=$Kr zcn@33<)p{<(|oR8)0WyUucY}b_~ehe8um-wyypK}{5)S@(EgcC8Iw44 zQOIO&sc1r>#4(8Z%e@x}!601GY+6G9voH6G#vaY-03yJ4jrKt}8K6_7=?CauvKCNU9YwdpeGs#W1Y9>^|XuPQ_cH&+ML;W~-iK;dGCs76U1ZT(= zR^z$N(h*#CVP~%E=oU0jEEDcxiJaRkjX7o|oJD{hYAuuMb@RbrM%xqF7<0b+pD_$$ z2RPlCZ=9yZpKRj;a7%x*XcJdI%NvaQKXH`ThSi=Od=naCbnV&ryOr~e812W%+FDM! zpWtj6)WBcAqFo;E3iyNUd09O_ENjV_jWi3KU=|f2!41#yo4KF{Q4o67M*FNyfSrmA=CD|% zpHNgc8j7O?@D=iE9Z2`&`*2r4{HueGC^rqesy(v;H}6}`^v|{Yf49P`1rs`bm6{Dl zrUYJ7B!B6atKUTUK(0NnhrWu(R4E0~Yre@r1adr_iXPLT4%25Z$_uz{So$Q-%V zG9Eys!0JnX8^h>yXaZ1JshP5$i8qzRXwd9rr^ESz``h-$im@|rzOs`P=1wDedU|?g z-xilALl`i%fy!>!Vbjupy$Z-54JLc^vVM(^e~E3;0k~G*34a{qq=fbqqhK@mQ%?*n zY_8FxijJj!Aq!=G`8$kGU8$){86YY@y|aA_lTundO$fm{Rv&Q)BY#K&z;7)8bTD{H z&Kkw+lmeB0fvYjK!+OYo*B(GG$qkTo`7gqF zYJ_V|LmQIsfypC%-tdnm6T<*f^5YfnxxUjU_ho=i6ipj~UGVc~@8=+^vBJPbHyN|Q zkdQxsU|26V`8Nw7^~yObD;r)XLJXD1@R_2a^F=h^pJz)A4!gRDvy=TZd>H)Gniu5j z&+R*U1cv@;)wcQJ>W~Q_1I9CL;BhV<0`_#<=9EjRL)C$JE)qTVe#G|L@kH|as23?K zErl_(q3aqlzNXvczKd}plp-xx^EJvBfyw_lA}2}(Y!G7ZKv(yZY4!qakTP<2kF+|2 za(jmU%9`E$tJoFRkT=gfZwx3G>>$o37vqHsQf7m_SJ!S26h*rqEE430@!o#?6sMZ%Jp_EulM!|i)=3r_)-yCu1|N~ z8PAg}YCW>6w&+$wy?6kx5xzH3XEh^3|AP+Dg*vL(tpPD}3`CEjVCvaCxw$lh*A>H1 zi`PR-{WK_sIUO4atx+_QGZYz4ki`v!?-~B*mF$cpKYV;L0lMZ50y;(Zno+Gy#*34k zM?*6@-76%>(y43~`l8@NJl{$l-czH|S$g$s&aV7ub<7v1YLa7Fp>kO{zD?ydMgRiQ zu-x}ptAT7RQ;nv*#-F6RX~F*%5b?aZ{o(`mC@Q zCG1Nu89{tNF`)L9C#OrayjhmQXangl%VD`i+iSmL0;MwW�U1Sw@eJ(vH^~umoH{ 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 XetuX>euyy{aH&7id06?tBKW@m8s_ww literal 0 HcmV?d00001 diff --git a/docs/index.md b/docs/index.md index 6f21f75023..0d1792402c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -373,8 +373,8 @@ After successfully adding and testing your credentials, Prowler will start scann #### **View Results** While the scan is running, start exploring the findings in these sections: -- **Overview**: High-level summary of the scans. Overview -- **Compliance**: Insights into compliance status. Compliance +- **Overview**: High-level summary of the scans. Overview +- **Compliance**: Insights into compliance status. Compliance > See more details about the Prowler App usage in the [Prowler App](tutorials/prowler-app.md) section. From 8c71a3948771b2ae313c33095b6a971bd355e0cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Mart=C3=ADn?= Date: Thu, 5 Dec 2024 11:01:51 +0100 Subject: [PATCH 02/67] docs(prowler-app): add link to https://api.prowler.com/api/v1/docs (#6016) --- docs/tutorials/prowler-app.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/tutorials/prowler-app.md b/docs/tutorials/prowler-app.md index 9133c8fe7b..e08fceb29b 100644 --- a/docs/tutorials/prowler-app.md +++ b/docs/tutorials/prowler-app.md @@ -5,6 +5,9 @@ The **Prowler App** is a user-friendly interface for the Prowler CLI, providing After [installing](../index.md#prowler-app-installation) the **Prowler App**, access it at [http://localhost:3000](http://localhost:3000). You can also access to the auto-generated **Prowler API** documentation at [http://localhost:8080/api/v1/docs](http://localhost:8080/api/v1/docs) to see all the available endpoints, parameters and responses. +???+ note + If you are a [Prowler Cloud](https://cloud.prowler.com/sign-in) user you can see API docs at [https://api.prowler.com/api/v1/docs](https://api.prowler.com/api/v1/docs) + ## **Step 1: Sign Up** To get started, sign up using your email and password: From eddc67226480f45f5e7035f5303fbbec9b78fa75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Mart=C3=ADn?= Date: Thu, 5 Dec 2024 13:51:13 +0100 Subject: [PATCH 03/67] chore(version): update prowler version (#6027) --- prowler/config/config.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/prowler/config/config.py b/prowler/config/config.py index 7771c4940b..bf018abb01 100644 --- a/prowler/config/config.py +++ b/prowler/config/config.py @@ -12,7 +12,7 @@ from prowler.lib.logger import logger timestamp = datetime.today() timestamp_utc = datetime.now(timezone.utc).replace(tzinfo=timezone.utc) -prowler_version = "5.0.0" +prowler_version = "5.1.0" html_logo_url = "https://github.com/prowler-cloud/prowler/" square_logo_img = "https://prowler.com/wp-content/uploads/logo-html.png" aws_logo = "https://user-images.githubusercontent.com/38561120/235953920-3e3fba08-0795-41dc-b480-9bea57db9f2e.png" diff --git a/pyproject.toml b/pyproject.toml index c72a5ab9a7..4903c018ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ packages = [ {include = "dashboard"} ] readme = "README.md" -version = "5.0.0" +version = "5.1.0" [tool.poetry.dependencies] alive-progress = "3.2.0" From ecfd94aeb14d29586274105355e2588cf48abff0 Mon Sep 17 00:00:00 2001 From: Pepe Fagoaga Date: Thu, 5 Dec 2024 16:35:56 +0100 Subject: [PATCH 04/67] fix(codecov): create components (#6028) --- .github/workflows/api-pull-request.yml | 15 +++++++++++++++ .github/workflows/pull-request.yml | 18 +++++++++++++++++- codecov.yml | 11 +++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 codecov.yml diff --git a/.github/workflows/api-pull-request.yml b/.github/workflows/api-pull-request.yml index 8143ca7f41..0c220c38fe 100644 --- a/.github/workflows/api-pull-request.yml +++ b/.github/workflows/api-pull-request.yml @@ -69,6 +69,7 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Test if changes are in not ignored paths id: are-non-ignored-files-changed uses: tj-actions/changed-files@v45 @@ -80,18 +81,21 @@ jobs: api/permissions/** api/README.md api/mkdocs.yml + - name: Install poetry working-directory: ./api if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' run: | python -m pip install --upgrade pip pipx install poetry + - name: Set up Python ${{ matrix.python-version }} if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: "poetry" + - name: Install dependencies working-directory: ./api if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' @@ -109,48 +113,59 @@ jobs: if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' run: | poetry lock --check + - name: Lint with ruff working-directory: ./api if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' run: | poetry run ruff check . --exclude contrib + - name: Check Format with ruff working-directory: ./api if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' run: | poetry run ruff format --check . --exclude contrib + - name: Lint with pylint working-directory: ./api if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' run: | poetry run pylint --disable=W,C,R,E -j 0 -rn -sn src/ + - name: Bandit working-directory: ./api if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' run: | poetry run bandit -q -lll -x '*_test.py,./contrib/' -r . + - name: Safety working-directory: ./api if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' run: | poetry run safety check --ignore 70612,66963 + - name: Vulture working-directory: ./api if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' run: | poetry run vulture --exclude "contrib,tests,conftest.py" --min-confidence 100 . + - name: Hadolint working-directory: ./api if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' run: | /tmp/hadolint Dockerfile --ignore=DL3013 + - name: Test with pytest working-directory: ./api if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' run: | poetry run pytest --cov=./src/backend --cov-report=xml src/backend + - name: Upload coverage reports to Codecov if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' uses: codecov/codecov-action@v5 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + with: + flags: api diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index cdb564838b..8512f20195 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -1,4 +1,4 @@ -name: "Pull Request" +name: "Pull Request" on: push: @@ -22,6 +22,7 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Test if changes are in not ignored paths id: are-non-ignored-files-changed uses: tj-actions/changed-files@v45 @@ -36,17 +37,20 @@ jobs: README.md mkdocs.yml .backportrc.json + - name: Install poetry if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' run: | python -m pip install --upgrade pip pipx install poetry + - name: Set up Python ${{ matrix.python-version }} if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: "poetry" + - name: Install dependencies if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' run: | @@ -57,44 +61,56 @@ jobs: sed -E 's/.*"v([^"]+)".*/\1/' \ ) && curl -L -o /tmp/hadolint "https://github.com/hadolint/hadolint/releases/download/v${VERSION}/hadolint-Linux-x86_64" \ && chmod +x /tmp/hadolint + - name: Poetry check if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' run: | poetry lock --check + - name: Lint with flake8 if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' run: | poetry run flake8 . --ignore=E266,W503,E203,E501,W605,E128 --exclude contrib,ui,api + - name: Checking format with black if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' run: | poetry run black --exclude api ui --check . + - name: Lint with pylint if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' run: | poetry run pylint --disable=W,C,R,E -j 0 -rn -sn prowler/ + - name: Bandit if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' run: | poetry run bandit -q -lll -x '*_test.py,./contrib/,./api/,./ui' -r . + - name: Safety if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' run: | poetry run safety check --ignore 70612 -r pyproject.toml + - name: Vulture if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' run: | poetry run vulture --exclude "contrib,api,ui" --min-confidence 100 . + - name: Hadolint if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' run: | /tmp/hadolint Dockerfile --ignore=DL3013 + - name: Test with pytest if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' run: | poetry run pytest -n auto --cov=./prowler --cov-report=xml tests + - name: Upload coverage reports to Codecov if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' uses: codecov/codecov-action@v5 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + with: + flags: prowler diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000000..ca31ca8dd4 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,11 @@ +component_management: + individual_components: + - component_id: "prowler" + paths: + - "prowler/**" + - component_id: "api" + paths: + - "api/**" + +comment: + layout: "header, diff, flags, components" From 2ce8e1fd21796dbd1291cd0642330b9068537e97 Mon Sep 17 00:00:00 2001 From: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com> Date: Fri, 6 Dec 2024 13:35:29 +0100 Subject: [PATCH 05/67] fix(backup): modify list recovery points call (#5996) --- .../aws/services/backup/backup_service.py | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/prowler/providers/aws/services/backup/backup_service.py b/prowler/providers/aws/services/backup/backup_service.py index 4805e36259..8690ae4ecc 100644 --- a/prowler/providers/aws/services/backup/backup_service.py +++ b/prowler/providers/aws/services/backup/backup_service.py @@ -183,21 +183,26 @@ class Backup(AWSService): def _list_recovery_points(self, regional_client): logger.info("Backup - Listing Recovery Points...") try: - for backup_vault in self.backup_vaults: - paginator = regional_client.get_paginator( - "list_recovery_points_by_backup_vault" - ) - for page in paginator.paginate(BackupVaultName=backup_vault.name): - for recovery_point in page.get("RecoveryPoints", []): - self.recovery_points.append( - RecoveryPoint( - arn=recovery_point.get("RecoveryPointArn"), - backup_vault_name=backup_vault.name, - encrypted=recovery_point.get("IsEncrypted", False), - backup_vault_region=backup_vault.region, - tags=[], - ) - ) + if self.backup_vaults: + for backup_vault in self.backup_vaults: + paginator = regional_client.get_paginator( + "list_recovery_points_by_backup_vault" + ) + for page in paginator.paginate(BackupVaultName=backup_vault.name): + for recovery_point in page.get("RecoveryPoints", []): + arn = recovery_point.get("RecoveryPointArn") + if arn: + self.recovery_points.append( + RecoveryPoint( + arn=arn, + backup_vault_name=backup_vault.name, + encrypted=recovery_point.get( + "IsEncrypted", False + ), + backup_vault_region=backup_vault.region, + tags=[], + ) + ) except ClientError as error: logger.error( f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" From bb0c346c4dd0d4a41099d0321097dbad8e95650a Mon Sep 17 00:00:00 2001 From: Prowler Bot Date: Fri, 6 Dec 2024 13:38:03 +0100 Subject: [PATCH 06/67] chore(regions_update): Changes in regions for AWS services (#6041) Co-authored-by: sergargar <38561120+sergargar@users.noreply.github.com> --- prowler/providers/aws/aws_regions_by_service.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/prowler/providers/aws/aws_regions_by_service.json b/prowler/providers/aws/aws_regions_by_service.json index cfc75a2e06..d58ae09536 100644 --- a/prowler/providers/aws/aws_regions_by_service.json +++ b/prowler/providers/aws/aws_regions_by_service.json @@ -10895,7 +10895,6 @@ "ca-west-1", "eu-central-1", "eu-central-2", - "eu-north-1", "eu-south-1", "eu-south-2", "eu-west-1", @@ -10903,7 +10902,6 @@ "eu-west-3", "il-central-1", "me-central-1", - "me-south-1", "sa-east-1", "us-east-1", "us-east-2", From 77c572f990f49492d5703b2181794047863cc8fa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 08:38:14 -0400 Subject: [PATCH 07/67] chore(deps): bump trufflesecurity/trufflehog from 3.84.1 to 3.85.0 (#6040) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/find-secrets.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/find-secrets.yml b/.github/workflows/find-secrets.yml index e73e874f26..f8d0b8c5f3 100644 --- a/.github/workflows/find-secrets.yml +++ b/.github/workflows/find-secrets.yml @@ -11,7 +11,7 @@ jobs: with: fetch-depth: 0 - name: TruffleHog OSS - uses: trufflesecurity/trufflehog@v3.84.1 + uses: trufflesecurity/trufflehog@v3.85.0 with: path: ./ base: ${{ github.event.repository.default_branch }} From a2ea216604904502c10c2171146b64415e280917 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 08:44:00 -0400 Subject: [PATCH 08/67] chore(deps): bump slack-sdk from 3.33.4 to 3.33.5 (#6039) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 10 +++++----- pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index 3261d5eaa4..5773686637 100644 --- a/poetry.lock +++ b/poetry.lock @@ -4638,17 +4638,17 @@ files = [ [[package]] name = "slack-sdk" -version = "3.33.4" +version = "3.33.5" description = "The Slack API Platform SDK for Python" optional = false python-versions = ">=3.6" files = [ - {file = "slack_sdk-3.33.4-py2.py3-none-any.whl", hash = "sha256:9f30cb3c9c07b441c49d53fc27f9f1837ad1592a7e9d4ca431f53cdad8826cc6"}, - {file = "slack_sdk-3.33.4.tar.gz", hash = "sha256:5e109847f6b6a22d227609226ba4ed936109dc00675bddeb7e0bee502d3ee7e0"}, + {file = "slack_sdk-3.33.5-py2.py3-none-any.whl", hash = "sha256:b8cccadfa3d4005a5e6529f52000d25c583f46173fda8e9136fdd2bc58923ff6"}, + {file = "slack_sdk-3.33.5.tar.gz", hash = "sha256:a5e74c00c99dc844ad93e501ab764a20d86fa8184bbc9432af217496f632c4ee"}, ] [package.extras] -optional = ["SQLAlchemy (>=1.4,<3)", "aiodns (>1.0)", "aiohttp (>=3.7.3,<4)", "boto3 (<=2)", "websocket-client (>=1,<2)", "websockets (>=9.1,<14)"] +optional = ["SQLAlchemy (>=1.4,<3)", "aiodns (>1.0)", "aiohttp (>=3.7.3,<4)", "boto3 (<=2)", "websocket-client (>=1,<2)", "websockets (>=9.1,<15)"] [[package]] name = "smmap" @@ -5194,4 +5194,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "1acc901866ecfc2c0f3576b9e442d7a3b6e6522cac3d4d1b9301ed4232755cba" +content-hash = "3c3e95351b98879864000e50303277a02996a9815b61d06af3e7574e5d4c34bf" diff --git a/pyproject.toml b/pyproject.toml index 4903c018ab..701ff13ca9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,7 +70,7 @@ python-dateutil = "^2.9.0.post0" pytz = "2024.2" schema = "0.7.7" shodan = "1.31.0" -slack-sdk = "3.33.4" +slack-sdk = "3.33.5" tabulate = "0.9.0" tzlocal = "5.2" From 263a7e21346f7ef1a6984fa2846e8242e97061b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 09:41:57 -0400 Subject: [PATCH 09/67] chore(deps): bump botocore from 1.35.71 to 1.35.76 (#6037) --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index 5773686637..611fe575cf 100644 --- a/poetry.lock +++ b/poetry.lock @@ -794,13 +794,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.35.71" +version = "1.35.76" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" files = [ - {file = "botocore-1.35.71-py3-none-any.whl", hash = "sha256:fc46e7ab1df3cef66dfba1633f4da77c75e07365b36f03bd64a3793634be8fc1"}, - {file = "botocore-1.35.71.tar.gz", hash = "sha256:f9fa058e0393660c3fe53c1e044751beb64b586def0bd2212448a7c328b0cbba"}, + {file = "botocore-1.35.76-py3-none-any.whl", hash = "sha256:b4729d12d00267b3185628f83543917b6caae292385230ab464067621aa086af"}, + {file = "botocore-1.35.76.tar.gz", hash = "sha256:a75a42ae53395796b8300c5fefb2d65a8696dc40dc85e49cf3a769e0c0202b13"}, ] [package.dependencies] @@ -5194,4 +5194,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "3c3e95351b98879864000e50303277a02996a9815b61d06af3e7574e5d4c34bf" +content-hash = "e6b5cb346e18c1a3afa74243699fc1d0af88c210f113e237a9a510ae0f896161" diff --git a/pyproject.toml b/pyproject.toml index 701ff13ca9..3e5742e801 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ azure-mgmt-subscription = "3.1.1" azure-mgmt-web = "7.3.1" azure-storage-blob = "12.24.0" boto3 = "1.35.71" -botocore = "1.35.71" +botocore = "1.35.76" colorama = "0.4.6" cryptography = "43.0.1" dash = "2.18.2" From 8f172aec8a48a02c014314fc243325d13f75aff1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 11:04:28 -0400 Subject: [PATCH 10/67] chore(deps-dev): bump pylint from 3.3.1 to 3.3.2 (#5993) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 10 +++++----- pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index 611fe575cf..6f07b6325d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3796,17 +3796,17 @@ tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] [[package]] name = "pylint" -version = "3.3.1" +version = "3.3.2" description = "python code static checker" optional = false python-versions = ">=3.9.0" files = [ - {file = "pylint-3.3.1-py3-none-any.whl", hash = "sha256:2f846a466dd023513240bc140ad2dd73bfc080a5d85a710afdb728c420a5a2b9"}, - {file = "pylint-3.3.1.tar.gz", hash = "sha256:9f3dcc87b1203e612b78d91a896407787e708b3f189b5fa0b307712d49ff0c6e"}, + {file = "pylint-3.3.2-py3-none-any.whl", hash = "sha256:77f068c287d49b8683cd7c6e624243c74f92890f767f106ffa1ddf3c0a54cb7a"}, + {file = "pylint-3.3.2.tar.gz", hash = "sha256:9ec054ec992cd05ad30a6df1676229739a73f8feeabf3912c995d17601052b01"}, ] [package.dependencies] -astroid = ">=3.3.4,<=3.4.0-dev0" +astroid = ">=3.3.5,<=3.4.0-dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = [ {version = ">=0.2", markers = "python_version < \"3.11\""}, @@ -5194,4 +5194,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "e6b5cb346e18c1a3afa74243699fc1d0af88c210f113e237a9a510ae0f896161" +content-hash = "2ba499f52912c4433c43b3c05f7baa19eae2467ed908d97bd52d70037797ac5f" diff --git a/pyproject.toml b/pyproject.toml index 3e5742e801..1ae0cf23fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,7 +85,7 @@ mock = "5.1.0" moto = {extras = ["all"], version = "5.0.16"} openapi-schema-validator = "0.6.2" openapi-spec-validator = "0.7.1" -pylint = "3.3.1" +pylint = "3.3.2" pytest = "8.3.3" pytest-cov = "6.0.0" pytest-env = "1.1.5" From c05c3396b5e0b4507b06112116222d5f7830b419 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 11:56:37 -0400 Subject: [PATCH 11/67] chore(deps-dev): bump mkdocs-material from 9.5.46 to 9.5.47 (#5988) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index 6f07b6325d..c4cce15f5d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2583,13 +2583,13 @@ dev = ["click", "codecov", "mkdocs-gen-files", "mkdocs-git-authors-plugin", "mkd [[package]] name = "mkdocs-material" -version = "9.5.46" +version = "9.5.47" description = "Documentation that simply works" optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_material-9.5.46-py3-none-any.whl", hash = "sha256:98f0a2039c62e551a68aad0791a8d41324ff90c03a6e6cea381a384b84908b83"}, - {file = "mkdocs_material-9.5.46.tar.gz", hash = "sha256:ae2043f4238e572f9a40e0b577f50400d6fc31e2fef8ea141800aebf3bd273d7"}, + {file = "mkdocs_material-9.5.47-py3-none-any.whl", hash = "sha256:53fb9c9624e7865da6ec807d116cd7be24b3cb36ab31b1d1d1a9af58c56009a2"}, + {file = "mkdocs_material-9.5.47.tar.gz", hash = "sha256:fc3b7a8e00ad896660bd3a5cc12ca0cb28bdc2bcbe2a946b5714c23ac91b0ede"}, ] [package.dependencies] @@ -5194,4 +5194,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "2ba499f52912c4433c43b3c05f7baa19eae2467ed908d97bd52d70037797ac5f" +content-hash = "c601352001cd7447ed320ad845c394003ece07a5153e986d8c34927ec261dcb9" diff --git a/pyproject.toml b/pyproject.toml index 1ae0cf23fb..c501c16d08 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -100,7 +100,7 @@ optional = true [tool.poetry.group.docs.dependencies] mkdocs = "1.6.1" mkdocs-git-revision-date-localized-plugin = "1.3.0" -mkdocs-material = "9.5.46" +mkdocs-material = "9.5.47" mkdocs-material-extensions = "1.3.1" [tool.poetry.scripts] From 203275817fcaf337c711ec5036d4af365b251f5e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 12:53:11 -0400 Subject: [PATCH 12/67] chore(deps-dev): bump pytest from 8.3.3 to 8.3.4 (#5992) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index c4cce15f5d..4e8c4410c4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3858,13 +3858,13 @@ diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" -version = "8.3.3" +version = "8.3.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, - {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, + {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, + {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, ] [package.dependencies] @@ -5194,4 +5194,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "c601352001cd7447ed320ad845c394003ece07a5153e986d8c34927ec261dcb9" +content-hash = "495e3a831e79082e0d73a3e42919ef4ae173eb541deb47cf6b58e34b4d612969" diff --git a/pyproject.toml b/pyproject.toml index c501c16d08..9bee7a3cb7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,7 +86,7 @@ moto = {extras = ["all"], version = "5.0.16"} openapi-schema-validator = "0.6.2" openapi-spec-validator = "0.7.1" pylint = "3.3.2" -pytest = "8.3.3" +pytest = "8.3.4" pytest-cov = "6.0.0" pytest-env = "1.1.5" pytest-randomly = "3.16.0" From 32cd39d158d9c780d54a096919adc5e95c8d3a12 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 20:29:06 -0400 Subject: [PATCH 13/67] chore(deps-dev): bump coverage from 7.6.8 to 7.6.9 (#6053) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 128 ++++++++++++++++++++++++------------------------- pyproject.toml | 2 +- 2 files changed, 65 insertions(+), 65 deletions(-) diff --git a/poetry.lock b/poetry.lock index 4e8c4410c4..68195be117 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1099,73 +1099,73 @@ files = [ [[package]] name = "coverage" -version = "7.6.8" +version = "7.6.9" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" files = [ - {file = "coverage-7.6.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b39e6011cd06822eb964d038d5dff5da5d98652b81f5ecd439277b32361a3a50"}, - {file = "coverage-7.6.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:63c19702db10ad79151a059d2d6336fe0c470f2e18d0d4d1a57f7f9713875dcf"}, - {file = "coverage-7.6.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3985b9be361d8fb6b2d1adc9924d01dec575a1d7453a14cccd73225cb79243ee"}, - {file = "coverage-7.6.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:644ec81edec0f4ad17d51c838a7d01e42811054543b76d4ba2c5d6af741ce2a6"}, - {file = "coverage-7.6.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f188a2402f8359cf0c4b1fe89eea40dc13b52e7b4fd4812450da9fcd210181d"}, - {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e19122296822deafce89a0c5e8685704c067ae65d45e79718c92df7b3ec3d331"}, - {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:13618bed0c38acc418896005732e565b317aa9e98d855a0e9f211a7ffc2d6638"}, - {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:193e3bffca48ad74b8c764fb4492dd875038a2f9925530cb094db92bb5e47bed"}, - {file = "coverage-7.6.8-cp310-cp310-win32.whl", hash = "sha256:3988665ee376abce49613701336544041f2117de7b7fbfe91b93d8ff8b151c8e"}, - {file = "coverage-7.6.8-cp310-cp310-win_amd64.whl", hash = "sha256:f56f49b2553d7dd85fd86e029515a221e5c1f8cb3d9c38b470bc38bde7b8445a"}, - {file = "coverage-7.6.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:86cffe9c6dfcfe22e28027069725c7f57f4b868a3f86e81d1c62462764dc46d4"}, - {file = "coverage-7.6.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d82ab6816c3277dc962cfcdc85b1efa0e5f50fb2c449432deaf2398a2928ab94"}, - {file = "coverage-7.6.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13690e923a3932e4fad4c0ebfb9cb5988e03d9dcb4c5150b5fcbf58fd8bddfc4"}, - {file = "coverage-7.6.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4be32da0c3827ac9132bb488d331cb32e8d9638dd41a0557c5569d57cf22c9c1"}, - {file = "coverage-7.6.8-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44e6c85bbdc809383b509d732b06419fb4544dca29ebe18480379633623baafb"}, - {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:768939f7c4353c0fac2f7c37897e10b1414b571fd85dd9fc49e6a87e37a2e0d8"}, - {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e44961e36cb13c495806d4cac67640ac2866cb99044e210895b506c26ee63d3a"}, - {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ea8bb1ab9558374c0ab591783808511d135a833c3ca64a18ec927f20c4030f0"}, - {file = "coverage-7.6.8-cp311-cp311-win32.whl", hash = "sha256:629a1ba2115dce8bf75a5cce9f2486ae483cb89c0145795603d6554bdc83e801"}, - {file = "coverage-7.6.8-cp311-cp311-win_amd64.whl", hash = "sha256:fb9fc32399dca861584d96eccd6c980b69bbcd7c228d06fb74fe53e007aa8ef9"}, - {file = "coverage-7.6.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e683e6ecc587643f8cde8f5da6768e9d165cd31edf39ee90ed7034f9ca0eefee"}, - {file = "coverage-7.6.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1defe91d41ce1bd44b40fabf071e6a01a5aa14de4a31b986aa9dfd1b3e3e414a"}, - {file = "coverage-7.6.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7ad66e8e50225ebf4236368cc43c37f59d5e6728f15f6e258c8639fa0dd8e6d"}, - {file = "coverage-7.6.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fe47da3e4fda5f1abb5709c156eca207eacf8007304ce3019eb001e7a7204cb"}, - {file = "coverage-7.6.8-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:202a2d645c5a46b84992f55b0a3affe4f0ba6b4c611abec32ee88358db4bb649"}, - {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4674f0daa1823c295845b6a740d98a840d7a1c11df00d1fd62614545c1583787"}, - {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:74610105ebd6f33d7c10f8907afed696e79c59e3043c5f20eaa3a46fddf33b4c"}, - {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37cda8712145917105e07aab96388ae76e787270ec04bcb9d5cc786d7cbb8443"}, - {file = "coverage-7.6.8-cp312-cp312-win32.whl", hash = "sha256:9e89d5c8509fbd6c03d0dd1972925b22f50db0792ce06324ba069f10787429ad"}, - {file = "coverage-7.6.8-cp312-cp312-win_amd64.whl", hash = "sha256:379c111d3558272a2cae3d8e57e6b6e6f4fe652905692d54bad5ea0ca37c5ad4"}, - {file = "coverage-7.6.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b0c69f4f724c64dfbfe79f5dfb503b42fe6127b8d479b2677f2b227478db2eb"}, - {file = "coverage-7.6.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c15b32a7aca8038ed7644f854bf17b663bc38e1671b5d6f43f9a2b2bd0c46f63"}, - {file = "coverage-7.6.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63068a11171e4276f6ece913bde059e77c713b48c3a848814a6537f35afb8365"}, - {file = "coverage-7.6.8-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f4548c5ead23ad13fb7a2c8ea541357474ec13c2b736feb02e19a3085fac002"}, - {file = "coverage-7.6.8-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b4b4299dd0d2c67caaaf286d58aef5e75b125b95615dda4542561a5a566a1e3"}, - {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9ebfb2507751f7196995142f057d1324afdab56db1d9743aab7f50289abd022"}, - {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c1b4474beee02ede1eef86c25ad4600a424fe36cff01a6103cb4533c6bf0169e"}, - {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d9fd2547e6decdbf985d579cf3fc78e4c1d662b9b0ff7cc7862baaab71c9cc5b"}, - {file = "coverage-7.6.8-cp313-cp313-win32.whl", hash = "sha256:8aae5aea53cbfe024919715eca696b1a3201886ce83790537d1c3668459c7146"}, - {file = "coverage-7.6.8-cp313-cp313-win_amd64.whl", hash = "sha256:ae270e79f7e169ccfe23284ff5ea2d52a6f401dc01b337efb54b3783e2ce3f28"}, - {file = "coverage-7.6.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de38add67a0af869b0d79c525d3e4588ac1ffa92f39116dbe0ed9753f26eba7d"}, - {file = "coverage-7.6.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b07c25d52b1c16ce5de088046cd2432b30f9ad5e224ff17c8f496d9cb7d1d451"}, - {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62a66ff235e4c2e37ed3b6104d8b478d767ff73838d1222132a7a026aa548764"}, - {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09b9f848b28081e7b975a3626e9081574a7b9196cde26604540582da60235fdf"}, - {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:093896e530c38c8e9c996901858ac63f3d4171268db2c9c8b373a228f459bbc5"}, - {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a7b8ac36fd688c8361cbc7bf1cb5866977ece6e0b17c34aa0df58bda4fa18a4"}, - {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:38c51297b35b3ed91670e1e4efb702b790002e3245a28c76e627478aa3c10d83"}, - {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2e4e0f60cb4bd7396108823548e82fdab72d4d8a65e58e2c19bbbc2f1e2bfa4b"}, - {file = "coverage-7.6.8-cp313-cp313t-win32.whl", hash = "sha256:6535d996f6537ecb298b4e287a855f37deaf64ff007162ec0afb9ab8ba3b8b71"}, - {file = "coverage-7.6.8-cp313-cp313t-win_amd64.whl", hash = "sha256:c79c0685f142ca53256722a384540832420dff4ab15fec1863d7e5bc8691bdcc"}, - {file = "coverage-7.6.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ac47fa29d8d41059ea3df65bd3ade92f97ee4910ed638e87075b8e8ce69599e"}, - {file = "coverage-7.6.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:24eda3a24a38157eee639ca9afe45eefa8d2420d49468819ac5f88b10de84f4c"}, - {file = "coverage-7.6.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4c81ed2820b9023a9a90717020315e63b17b18c274a332e3b6437d7ff70abe0"}, - {file = "coverage-7.6.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd55f8fc8fa494958772a2a7302b0354ab16e0b9272b3c3d83cdb5bec5bd1779"}, - {file = "coverage-7.6.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f39e2f3530ed1626c66e7493be7a8423b023ca852aacdc91fb30162c350d2a92"}, - {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:716a78a342679cd1177bc8c2fe957e0ab91405bd43a17094324845200b2fddf4"}, - {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:177f01eeaa3aee4a5ffb0d1439c5952b53d5010f86e9d2667963e632e30082cc"}, - {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:912e95017ff51dc3d7b6e2be158dedc889d9a5cc3382445589ce554f1a34c0ea"}, - {file = "coverage-7.6.8-cp39-cp39-win32.whl", hash = "sha256:4db3ed6a907b555e57cc2e6f14dc3a4c2458cdad8919e40b5357ab9b6db6c43e"}, - {file = "coverage-7.6.8-cp39-cp39-win_amd64.whl", hash = "sha256:428ac484592f780e8cd7b6b14eb568f7c85460c92e2a37cb0c0e5186e1a0d076"}, - {file = "coverage-7.6.8-pp39.pp310-none-any.whl", hash = "sha256:5c52a036535d12590c32c49209e79cabaad9f9ad8aa4cbd875b68c4d67a9cbce"}, - {file = "coverage-7.6.8.tar.gz", hash = "sha256:8b2b8503edb06822c86d82fa64a4a5cb0760bb8f31f26e138ec743f422f37cfc"}, + {file = "coverage-7.6.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85d9636f72e8991a1706b2b55b06c27545448baf9f6dbf51c4004609aacd7dcb"}, + {file = "coverage-7.6.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:608a7fd78c67bee8936378299a6cb9f5149bb80238c7a566fc3e6717a4e68710"}, + {file = "coverage-7.6.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96d636c77af18b5cb664ddf12dab9b15a0cfe9c0bde715da38698c8cea748bfa"}, + {file = "coverage-7.6.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75cded8a3cff93da9edc31446872d2997e327921d8eed86641efafd350e1df1"}, + {file = "coverage-7.6.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7b15f589593110ae767ce997775d645b47e5cbbf54fd322f8ebea6277466cec"}, + {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:44349150f6811b44b25574839b39ae35291f6496eb795b7366fef3bd3cf112d3"}, + {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d891c136b5b310d0e702e186d70cd16d1119ea8927347045124cb286b29297e5"}, + {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:db1dab894cc139f67822a92910466531de5ea6034ddfd2b11c0d4c6257168073"}, + {file = "coverage-7.6.9-cp310-cp310-win32.whl", hash = "sha256:41ff7b0da5af71a51b53f501a3bac65fb0ec311ebed1632e58fc6107f03b9198"}, + {file = "coverage-7.6.9-cp310-cp310-win_amd64.whl", hash = "sha256:35371f8438028fdccfaf3570b31d98e8d9eda8bb1d6ab9473f5a390969e98717"}, + {file = "coverage-7.6.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:932fc826442132dde42ee52cf66d941f581c685a6313feebed358411238f60f9"}, + {file = "coverage-7.6.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:085161be5f3b30fd9b3e7b9a8c301f935c8313dcf928a07b116324abea2c1c2c"}, + {file = "coverage-7.6.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc660a77e1c2bf24ddbce969af9447a9474790160cfb23de6be4fa88e3951c7"}, + {file = "coverage-7.6.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c69e42c892c018cd3c8d90da61d845f50a8243062b19d228189b0224150018a9"}, + {file = "coverage-7.6.9-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0824a28ec542a0be22f60c6ac36d679e0e262e5353203bea81d44ee81fe9c6d4"}, + {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4401ae5fc52ad8d26d2a5d8a7428b0f0c72431683f8e63e42e70606374c311a1"}, + {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98caba4476a6c8d59ec1eb00c7dd862ba9beca34085642d46ed503cc2d440d4b"}, + {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ee5defd1733fd6ec08b168bd4f5387d5b322f45ca9e0e6c817ea6c4cd36313e3"}, + {file = "coverage-7.6.9-cp311-cp311-win32.whl", hash = "sha256:f2d1ec60d6d256bdf298cb86b78dd715980828f50c46701abc3b0a2b3f8a0dc0"}, + {file = "coverage-7.6.9-cp311-cp311-win_amd64.whl", hash = "sha256:0d59fd927b1f04de57a2ba0137166d31c1a6dd9e764ad4af552912d70428c92b"}, + {file = "coverage-7.6.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:99e266ae0b5d15f1ca8d278a668df6f51cc4b854513daab5cae695ed7b721cf8"}, + {file = "coverage-7.6.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9901d36492009a0a9b94b20e52ebfc8453bf49bb2b27bca2c9706f8b4f5a554a"}, + {file = "coverage-7.6.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abd3e72dd5b97e3af4246cdada7738ef0e608168de952b837b8dd7e90341f015"}, + {file = "coverage-7.6.9-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff74026a461eb0660366fb01c650c1d00f833a086b336bdad7ab00cc952072b3"}, + {file = "coverage-7.6.9-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65dad5a248823a4996724a88eb51d4b31587aa7aa428562dbe459c684e5787ae"}, + {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:22be16571504c9ccea919fcedb459d5ab20d41172056206eb2994e2ff06118a4"}, + {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f957943bc718b87144ecaee70762bc2bc3f1a7a53c7b861103546d3a403f0a6"}, + {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ae1387db4aecb1f485fb70a6c0148c6cdaebb6038f1d40089b1fc84a5db556f"}, + {file = "coverage-7.6.9-cp312-cp312-win32.whl", hash = "sha256:1a330812d9cc7ac2182586f6d41b4d0fadf9be9049f350e0efb275c8ee8eb692"}, + {file = "coverage-7.6.9-cp312-cp312-win_amd64.whl", hash = "sha256:b12c6b18269ca471eedd41c1b6a1065b2f7827508edb9a7ed5555e9a56dcfc97"}, + {file = "coverage-7.6.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:899b8cd4781c400454f2f64f7776a5d87bbd7b3e7f7bda0cb18f857bb1334664"}, + {file = "coverage-7.6.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:61f70dc68bd36810972e55bbbe83674ea073dd1dcc121040a08cdf3416c5349c"}, + {file = "coverage-7.6.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a289d23d4c46f1a82d5db4abeb40b9b5be91731ee19a379d15790e53031c014"}, + {file = "coverage-7.6.9-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e216d8044a356fc0337c7a2a0536d6de07888d7bcda76febcb8adc50bdbbd00"}, + {file = "coverage-7.6.9-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c026eb44f744acaa2bda7493dad903aa5bf5fc4f2554293a798d5606710055d"}, + {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e77363e8425325384f9d49272c54045bbed2f478e9dd698dbc65dbc37860eb0a"}, + {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:777abfab476cf83b5177b84d7486497e034eb9eaea0d746ce0c1268c71652077"}, + {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:447af20e25fdbe16f26e84eb714ba21d98868705cb138252d28bc400381f6ffb"}, + {file = "coverage-7.6.9-cp313-cp313-win32.whl", hash = "sha256:d872ec5aeb086cbea771c573600d47944eea2dcba8be5f3ee649bfe3cb8dc9ba"}, + {file = "coverage-7.6.9-cp313-cp313-win_amd64.whl", hash = "sha256:fd1213c86e48dfdc5a0cc676551db467495a95a662d2396ecd58e719191446e1"}, + {file = "coverage-7.6.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9e7484d286cd5a43744e5f47b0b3fb457865baf07bafc6bee91896364e1419"}, + {file = "coverage-7.6.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1cf0872ee455c03e5674b5bca5e3e68e159379c1af0903e89f5eba9ccc3a"}, + {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d10e07aa2b91835d6abec555ec8b2733347956991901eea6ffac295f83a30e4"}, + {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13a9e2d3ee855db3dd6ea1ba5203316a1b1fd8eaeffc37c5b54987e61e4194ae"}, + {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c38bf15a40ccf5619fa2fe8f26106c7e8e080d7760aeccb3722664c8656b030"}, + {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d5275455b3e4627c8e7154feaf7ee0743c2e7af82f6e3b561967b1cca755a0be"}, + {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8f8770dfc6e2c6a2d4569f411015c8d751c980d17a14b0530da2d7f27ffdd88e"}, + {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8d2dfa71665a29b153a9681edb1c8d9c1ea50dfc2375fb4dac99ea7e21a0bcd9"}, + {file = "coverage-7.6.9-cp313-cp313t-win32.whl", hash = "sha256:5e6b86b5847a016d0fbd31ffe1001b63355ed309651851295315031ea7eb5a9b"}, + {file = "coverage-7.6.9-cp313-cp313t-win_amd64.whl", hash = "sha256:97ddc94d46088304772d21b060041c97fc16bdda13c6c7f9d8fcd8d5ae0d8611"}, + {file = "coverage-7.6.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:adb697c0bd35100dc690de83154627fbab1f4f3c0386df266dded865fc50a902"}, + {file = "coverage-7.6.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:be57b6d56e49c2739cdf776839a92330e933dd5e5d929966fbbd380c77f060be"}, + {file = "coverage-7.6.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1592791f8204ae9166de22ba7e6705fa4ebd02936c09436a1bb85aabca3e599"}, + {file = "coverage-7.6.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e12ae8cc979cf83d258acb5e1f1cf2f3f83524d1564a49d20b8bec14b637f08"}, + {file = "coverage-7.6.9-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb5555cff66c4d3d6213a296b360f9e1a8e323e74e0426b6c10ed7f4d021e464"}, + {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b9389a429e0e5142e69d5bf4a435dd688c14478a19bb901735cdf75e57b13845"}, + {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:592ac539812e9b46046620341498caf09ca21023c41c893e1eb9dbda00a70cbf"}, + {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a27801adef24cc30871da98a105f77995e13a25a505a0161911f6aafbd66e678"}, + {file = "coverage-7.6.9-cp39-cp39-win32.whl", hash = "sha256:8e3c3e38930cfb729cb8137d7f055e5a473ddaf1217966aa6238c88bd9fd50e6"}, + {file = "coverage-7.6.9-cp39-cp39-win_amd64.whl", hash = "sha256:e28bf44afa2b187cc9f41749138a64435bf340adfcacb5b2290c070ce99839d4"}, + {file = "coverage-7.6.9-pp39.pp310-none-any.whl", hash = "sha256:f3ca78518bc6bc92828cd11867b121891d75cae4ea9e908d72030609b996db1b"}, + {file = "coverage-7.6.9.tar.gz", hash = "sha256:4a8d8977b0c6ef5aeadcb644da9e69ae0dcfe66ec7f368c89c72e058bd71164d"}, ] [package.dependencies] @@ -5194,4 +5194,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "495e3a831e79082e0d73a3e42919ef4ae173eb541deb47cf6b58e34b4d612969" +content-hash = "e7a255ff9cefb4b377b3797a46c253d3707507ca139b24a79848b278ec717238" diff --git a/pyproject.toml b/pyproject.toml index 9bee7a3cb7..b77fddd646 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,7 +77,7 @@ tzlocal = "5.2" [tool.poetry.group.dev.dependencies] bandit = "1.8.0" black = "24.10.0" -coverage = "7.6.8" +coverage = "7.6.9" docker = "7.1.0" flake8 = "7.1.1" freezegun = "1.5.1" From b6c3ba0f0d3f20b47a2e8f832c11db6a910e69c0 Mon Sep 17 00:00:00 2001 From: Pepe Fagoaga Date: Mon, 9 Dec 2024 09:07:10 +0100 Subject: [PATCH 14/67] chore: delete unneeded requirements file (#6056) --- ui/requirements.txt | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 ui/requirements.txt diff --git a/ui/requirements.txt b/ui/requirements.txt deleted file mode 100644 index 4dcab051c2..0000000000 --- a/ui/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -Django==5.0.9 -djangorestframework==3.15.2 -django-cors-headers==4.3.1 From d8fc830f1db874b2fef6eea7d9c8718f09dbadc9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 10:11:51 +0100 Subject: [PATCH 15/67] chore(deps): bump boto3 from 1.35.71 to 1.35.76 (#6054) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 10 +++++----- pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index 68195be117..cb022c0885 100644 --- a/poetry.lock +++ b/poetry.lock @@ -775,17 +775,17 @@ files = [ [[package]] name = "boto3" -version = "1.35.71" +version = "1.35.76" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" files = [ - {file = "boto3-1.35.71-py3-none-any.whl", hash = "sha256:e2969a246bb3208122b3c349c49cc6604c6fc3fc2b2f65d99d3e8ccd745b0c16"}, - {file = "boto3-1.35.71.tar.gz", hash = "sha256:3ed7172b3d4fceb6218bb0ec3668c4d40c03690939c2fca4f22bb875d741a07f"}, + {file = "boto3-1.35.76-py3-none-any.whl", hash = "sha256:69458399f41f57a50770c8974796d96978bcca44915c260319696bb43e47dffd"}, + {file = "boto3-1.35.76.tar.gz", hash = "sha256:31ddcdb6f15dace2b68f6a0f11bdb58dd3ae79b8a3ccb174ff811ef0bbf938e0"}, ] [package.dependencies] -botocore = ">=1.35.71,<1.36.0" +botocore = ">=1.35.76,<1.36.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.10.0,<0.11.0" @@ -5194,4 +5194,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "e7a255ff9cefb4b377b3797a46c253d3707507ca139b24a79848b278ec717238" +content-hash = "af11b4809a220ca4704bdff59e55883d4d87661a2fcb301f073ab807a8c29193" diff --git a/pyproject.toml b/pyproject.toml index b77fddd646..d6329e51ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ azure-mgmt-storage = "21.2.1" azure-mgmt-subscription = "3.1.1" azure-mgmt-web = "7.3.1" azure-storage-blob = "12.24.0" -boto3 = "1.35.71" +boto3 = "1.35.76" botocore = "1.35.76" colorama = "0.4.6" cryptography = "43.0.1" From 493fe2d52300b85143cb261214a004e435b5c24b Mon Sep 17 00:00:00 2001 From: Sergio Garcia Date: Mon, 9 Dec 2024 06:11:05 -0400 Subject: [PATCH 16/67] docs(env): move warning about env files (#6049) --- docs/index.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/index.md b/docs/index.md index 0d1792402c..7779752fc7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -29,7 +29,7 @@ It contains hundreds of controls covering CIS, NIST 800, NIST CSF, CISA, RBI, Fe Prowler App can be installed in different ways, depending on your environment: -> See how to use Prowler App in the [Prowler App](tutorials/prowler-app.md) section. +> See how to use Prowler App in the [Prowler App Tutorial](tutorials/prowler-app.md) section. === "Docker Compose" @@ -65,6 +65,9 @@ Prowler App can be installed in different ways, depending on your environment: * `npm` installed: [npm installation](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). * `Docker Compose` installed: https://docs.docker.com/compose/install/. + ???+ warning + Make sure to have `api/.env` and `ui/.env.local` files with the required environment variables. You can find the required environment variables in the [`api/.env.template`](https://github.com/prowler-cloud/prowler/blob/master/api/.env.example) and [`ui/.env.template`](https://github.com/prowler-cloud/prowler/blob/master/ui/.env.template) files. + _Commands to run the API_: ``` bash @@ -107,9 +110,6 @@ Prowler App can be installed in different ways, depending on your environment: > Enjoy Prowler App at http://localhost:3000 by signing up with your email and password. - ???+ warning - Make sure to have `api/.env` and `ui/.env.local` files with the required environment variables. You can find the required environment variables in the [`api/.env.template`](https://github.com/prowler-cloud/prowler/blob/master/api/.env.example) and [`ui/.env.template`](https://github.com/prowler-cloud/prowler/blob/master/ui/.env.template) files. - ???+ warning Google and GitHub authentication is only available in [Prowler Cloud](https://prowler.com). From fefe89a1ed14c464314082e8aa0615fa83654c39 Mon Sep 17 00:00:00 2001 From: Pepe Fagoaga Date: Mon, 9 Dec 2024 14:12:08 +0100 Subject: [PATCH 17/67] fix(backport): Add action to detect labels (#5270) --- .github/workflows/backport.yml | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 534f7d6d46..87dfd5eed6 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -5,38 +5,41 @@ on: branches: ['master'] types: ['labeled', 'closed'] +env: + BACKPORT_LABEL_PREFIX: backport-to-v + BACKPORT_LABEL_IGNORE: was-backported + jobs: backport: name: Backport PR - if: github.event.pull_request.merged == true && !(contains(github.event.pull_request.labels.*.name, 'backport')) + if: github.event.pull_request.merged == true && !(contains(github.event.pull_request.labels.*.name, 'backport')) && !(contains(github.event.pull_request.labels.*.name, 'was-backported')) runs-on: ubuntu-latest permissions: id-token: write pull-requests: write contents: write steps: - # Workaround not to fail the workflow if the PR does not need a backport - # https://github.com/sorenlouv/backport-github-action/issues/127#issuecomment-2258561266 - - name: Check for backport labels - id: check_labels - run: |- - labels='${{ toJSON(github.event.pull_request.labels.*.name) }}' - echo "$labels" - matched=$(echo "${labels}" | jq '. | map(select(startswith("backport-to-"))) | length') - echo "matched=$matched" - echo "matched=$matched" >> $GITHUB_OUTPUT + - name: Check labels + id: preview_label_check + uses: docker://agilepathway/pull-request-label-checker:v1.6.55 + with: + allow_failure: true + prefix_mode: true + one_of: ${{ env.BACKPORT_LABEL_PREFIX}} + none_of: ${{ env.BACKPORT_LABEL_IGNORE}} + repo_token: ${{ secrets.GITHUB_TOKEN }} - name: Backport Action - if: fromJSON(steps.check_labels.outputs.matched) > 0 + if: steps.preview_label_check.outputs.label_check == 'success' uses: sorenlouv/backport-github-action@v9.5.1 with: github_token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} - auto_backport_label_prefix: backport-to- + auto_backport_label_prefix: ${{ env.BACKPORT_LABEL_PREFIX}} - name: Info log - if: ${{ success() && fromJSON(steps.check_labels.outputs.matched) > 0 }} + if: ${{ success() && steps.preview_label_check.outputs.label_check == 'success' }} run: cat ~/.backport/backport.info.log - name: Debug log - if: ${{ failure() && fromJSON(steps.check_labels.outputs.matched) > 0 }} + if: ${{ failure() && steps.preview_label_check.outputs.label_check == 'success' }} run: cat ~/.backport/backport.debug.log From a8a567c588b49b69db79e53130bf5255faac5c88 Mon Sep 17 00:00:00 2001 From: Pepe Fagoaga Date: Mon, 9 Dec 2024 14:12:54 +0100 Subject: [PATCH 18/67] docs: Prowler SaaS -> Cloud and add missing compliance (#6061) --- README.md | 4 ++-- docs/index.md | 2 +- docs/security.md | 2 +- docs/tutorials/compliance.md | 28 ++++++++++++++++++++++------ mkdocs.yml | 3 +-- 5 files changed, 27 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 1a0944fb96..2e1a7cfeb5 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@

- Prowler SaaS and Prowler Open Source are as dynamic and adaptable as the environment they’re meant to protect. Trusted by the leaders in security. + Prowler Open Source is as dynamic and adaptable as the environment they’re meant to protect. Trusted by the leaders in security.

Learn more at prowler.com @@ -43,7 +43,7 @@ # Description -**Prowler** is an Open Source security tool to perform AWS, Azure, Google Cloud and Kubernetes security best practices assessments, audits, incident response, continuous monitoring, hardening and forensics readiness, and also remediations! We have Prowler CLI (Command Line Interface) that we call Prowler Open Source and a service on top of it that we call Prowler SaaS. +**Prowler** is an Open Source security tool to perform AWS, Azure, Google Cloud and Kubernetes security best practices assessments, audits, incident response, continuous monitoring, hardening and forensics readiness, and also remediations! We have Prowler CLI (Command Line Interface) that we call Prowler Open Source and a service on top of it that we call Prowler Cloud. ## Prowler App diff --git a/docs/index.md b/docs/index.md index 7779752fc7..8dc5f08ffc 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,4 +1,4 @@ -**Prowler** is an Open Source security tool to perform AWS, Azure, Google Cloud and Kubernetes security best practices assessments, audits, incident response, continuous monitoring, hardening and forensics readiness, and also remediations! We have Prowler CLI (Command Line Interface) that we call Prowler Open Source and a service on top of it that we call Prowler SaaS. +**Prowler** is an Open Source security tool to perform AWS, Azure, Google Cloud and Kubernetes security best practices assessments, audits, incident response, continuous monitoring, hardening and forensics readiness, and also remediations! We have Prowler CLI (Command Line Interface) that we call Prowler Open Source and a service on top of it that we call Prowler Cloud. ## Prowler App diff --git a/docs/security.md b/docs/security.md index ef85c357a8..207b565378 100644 --- a/docs/security.md +++ b/docs/security.md @@ -13,7 +13,7 @@ As an **AWS Partner** and we have passed the [AWS Foundation Technical Review (F ## Reporting Vulnerabilities -If you would like to report a vulnerability or have a security concern regarding Prowler Open Source or Prowler SaaS service, please submit the information by contacting to us via [**support.prowler.com**](http://support.prowler.com). +If you would like to report a vulnerability or have a security concern regarding Prowler Open Source or Prowler Cloud service, please submit the information by contacting to us via [**support.prowler.com**](http://support.prowler.com). The information you share with the Prowler team as part of this process is kept confidential within Prowler. We will only share this information with a third party if the vulnerability you report is found to affect a third-party product, in which case we will share this information with the third-party product's author or manufacturer. Otherwise, we will only share this information as permitted by you. diff --git a/docs/tutorials/compliance.md b/docs/tutorials/compliance.md index 629aec9b06..b3424dbf27 100644 --- a/docs/tutorials/compliance.md +++ b/docs/tutorials/compliance.md @@ -22,32 +22,31 @@ In order to see which compliance frameworks are cover by Prowler, you can use op ```sh prowler --list-compliance ``` -Currently, the available frameworks are: + +### AWS - `aws_account_security_onboarding_aws` - `aws_audit_manager_control_tower_guardrails_aws` - `aws_foundational_security_best_practices_aws` +- `aws_foundational_technical_review_aws` - `aws_well_architected_framework_reliability_pillar_aws` - `aws_well_architected_framework_security_pillar_aws` - `cis_1.4_aws` - `cis_1.5_aws` - `cis_2.0_aws` -- `cis_2.0_gcp` -- `cis_2.0_azure` -- `cis_2.1_azure` - `cis_3.0_aws` -- `cis_1.8_kubernetes` - `cisa_aws` - `ens_rd2022_aws` - `fedramp_low_revision_4_aws` - `fedramp_moderate_revision_4_aws` - `ffiec_aws` -- `aws_foundational_technical_review_aws` - `gdpr_aws` - `gxp_21_cfr_part_11_aws` - `gxp_eu_annex_11_aws` - `hipaa_aws` - `iso27001_2013_aws` +- `kisa_isms_p_2023_aws` +- `kisa_isms_p_2023_korean_aws` - `mitre_attack_aws` - `nist_800_171_revision_2_aws` - `nist_800_53_revision_4_aws` @@ -57,6 +56,23 @@ Currently, the available frameworks are: - `rbi_cyber_security_framework_aws` - `soc2_aws` +### Azure + +- `cis_2.0_azure` +- `cis_2.1_azure` +- `ens_rd2022_azure` +- `mitre_attack_azure` + +### GCP + +- `cis_2.0_gcp` +- `ens_rd2022_gcp` +- `mitre_attack_gcp` + +### Kubernetes + +- `cis_1.8_kubernetes` + ## List Requirements of Compliance Frameworks For each compliance framework, you can use option `--list-compliance-requirements` to list its requirements: ```sh diff --git a/mkdocs.yml b/mkdocs.yml index e311146609..dd04e16881 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -34,7 +34,6 @@ theme: icon: material/weather-sunny name: Switch to light mode - plugins: - search - git-revision-date-localized: @@ -112,7 +111,7 @@ nav: - Contact Us: contact.md - Troubleshooting: troubleshooting.md - About: about.md - - Prowler SaaS: https://prowler.com + - Prowler Cloud: https://prowler.com # Customization extra: From 213a793fbcb94af0d502b0005c70ff4dbc684279 Mon Sep 17 00:00:00 2001 From: Pepe Fagoaga Date: Mon, 9 Dec 2024 14:14:06 +0100 Subject: [PATCH 19/67] chore(actions): standardize names (#6059) --- .github/codeql/api-codeql-config.yml | 4 ++-- .github/codeql/codeql-config.yml | 4 ---- .github/codeql/sdk-codeql-config.yml | 4 ++++ .github/codeql/ui-codeql-config.yml | 2 +- .github/workflows/api-codeql.yml | 6 +++--- .github/workflows/api-pull-request.yml | 2 +- .github/workflows/backport.yml | 2 +- .github/workflows/build-documentation-on-pr.yml | 2 +- .github/workflows/find-secrets.yml | 4 ++-- .github/workflows/labeler.yml | 2 +- ...-containers.yml => sdk-build-lint-push-containers.yml} | 6 +++--- .github/workflows/{codeql.yml => sdk-codeql.yml} | 8 ++++---- .../workflows/{pull-request.yml => sdk-pull-request.yml} | 2 +- .../workflows/{pypi-release.yml => sdk-pypi-release.yml} | 2 +- ...s_regions.yml => sdk-refresh-aws-services-regions.yml} | 2 +- .github/workflows/ui-codeql.yml | 2 +- .github/workflows/{ui-checks.yml => ui-pull-request.yml} | 4 ++-- 17 files changed, 29 insertions(+), 29 deletions(-) delete mode 100644 .github/codeql/codeql-config.yml create mode 100644 .github/codeql/sdk-codeql-config.yml rename .github/workflows/{build-lint-push-containers.yml => sdk-build-lint-push-containers.yml} (99%) rename .github/workflows/{codeql.yml => sdk-codeql.yml} (93%) rename .github/workflows/{pull-request.yml => sdk-pull-request.yml} (99%) rename .github/workflows/{pypi-release.yml => sdk-pypi-release.yml} (98%) rename .github/workflows/{refresh_aws_services_regions.yml => sdk-refresh-aws-services-regions.yml} (98%) rename .github/workflows/{ui-checks.yml => ui-pull-request.yml} (93%) diff --git a/.github/codeql/api-codeql-config.yml b/.github/codeql/api-codeql-config.yml index ac8ca6beb5..9ce26a3651 100644 --- a/.github/codeql/api-codeql-config.yml +++ b/.github/codeql/api-codeql-config.yml @@ -1,3 +1,3 @@ -name: "Custom CodeQL Config for API" +name: "API - CodeQL Config" paths: - - 'api/' \ No newline at end of file + - "api/" diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml deleted file mode 100644 index f31f68cba8..0000000000 --- a/.github/codeql/codeql-config.yml +++ /dev/null @@ -1,4 +0,0 @@ -name: "Custom CodeQL Config" -paths-ignore: - - 'api/' - - 'ui/' \ No newline at end of file diff --git a/.github/codeql/sdk-codeql-config.yml b/.github/codeql/sdk-codeql-config.yml new file mode 100644 index 0000000000..7982398f42 --- /dev/null +++ b/.github/codeql/sdk-codeql-config.yml @@ -0,0 +1,4 @@ +name: "SDK - CodeQL Config" +paths-ignore: + - "api/" + - "ui/" diff --git a/.github/codeql/ui-codeql-config.yml b/.github/codeql/ui-codeql-config.yml index 62ebee5617..fa4f80cae5 100644 --- a/.github/codeql/ui-codeql-config.yml +++ b/.github/codeql/ui-codeql-config.yml @@ -1,3 +1,3 @@ -name: "Custom CodeQL Config for UI" +name: "UI - CodeQL Config" paths: - "ui/" diff --git a/.github/workflows/api-codeql.yml b/.github/workflows/api-codeql.yml index ed9e0c3fd4..75d12109a6 100644 --- a/.github/workflows/api-codeql.yml +++ b/.github/workflows/api-codeql.yml @@ -9,11 +9,11 @@ # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # -name: "API - CodeQL" +name: API - CodeQL on: push: - branches: + branches: - "master" - "v3" - "v4.*" @@ -21,7 +21,7 @@ on: paths: - "api/**" pull_request: - branches: + branches: - "master" - "v3" - "v4.*" diff --git a/.github/workflows/api-pull-request.yml b/.github/workflows/api-pull-request.yml index 0c220c38fe..896be36f7a 100644 --- a/.github/workflows/api-pull-request.yml +++ b/.github/workflows/api-pull-request.yml @@ -1,4 +1,4 @@ -name: "API - Pull Request" +name: API - Pull Request on: push: diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 87dfd5eed6..6639b526fb 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -1,4 +1,4 @@ -name: Automatic Backport +name: Prowler - Automatic Backport on: pull_request_target: diff --git a/.github/workflows/build-documentation-on-pr.yml b/.github/workflows/build-documentation-on-pr.yml index a1ed22a071..7ae58b9c85 100644 --- a/.github/workflows/build-documentation-on-pr.yml +++ b/.github/workflows/build-documentation-on-pr.yml @@ -1,4 +1,4 @@ -name: Pull Request Documentation Link +name: Prowler - Pull Request Documentation Link on: pull_request: diff --git a/.github/workflows/find-secrets.yml b/.github/workflows/find-secrets.yml index f8d0b8c5f3..c89d9d0fae 100644 --- a/.github/workflows/find-secrets.yml +++ b/.github/workflows/find-secrets.yml @@ -1,4 +1,4 @@ -name: Find secrets +name: Prowler - Find secrets on: pull_request @@ -16,4 +16,4 @@ jobs: path: ./ base: ${{ github.event.repository.default_branch }} head: HEAD - extra_args: --only-verified \ No newline at end of file + extra_args: --only-verified diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index d7162a0058..199b17962d 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -1,4 +1,4 @@ -name: "Pull Request Labeler" +name: Prowler - PR Labeler on: pull_request_target: diff --git a/.github/workflows/build-lint-push-containers.yml b/.github/workflows/sdk-build-lint-push-containers.yml similarity index 99% rename from .github/workflows/build-lint-push-containers.yml rename to .github/workflows/sdk-build-lint-push-containers.yml index af17eeb193..b502af0ba4 100644 --- a/.github/workflows/build-lint-push-containers.yml +++ b/.github/workflows/sdk-build-lint-push-containers.yml @@ -1,4 +1,4 @@ -name: Build and Push containers +name: SDK - Build and Push containers on: push: @@ -85,8 +85,8 @@ jobs: echo "STABLE_TAG=v3-stable" >> "${GITHUB_ENV}" ;; - - 4) + + 4) echo "LATEST_TAG=v4-latest" >> "${GITHUB_ENV}" echo "STABLE_TAG=v4-stable" >> "${GITHUB_ENV}" ;; diff --git a/.github/workflows/codeql.yml b/.github/workflows/sdk-codeql.yml similarity index 93% rename from .github/workflows/codeql.yml rename to .github/workflows/sdk-codeql.yml index c0e319e07e..043aeb041b 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/sdk-codeql.yml @@ -9,11 +9,11 @@ # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # -name: "CodeQL" +name: SDK - CodeQL on: push: - branches: + branches: - "master" - "v3" - "v4.*" @@ -21,7 +21,7 @@ on: - 'ui/**' - 'api/**' pull_request: - branches: + branches: - "master" - "v3" - "v4.*" @@ -55,7 +55,7 @@ jobs: uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} - config-file: ./.github/codeql/codeql-config.yml + config-file: ./.github/codeql/sdk-codeql-config.yml - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/pull-request.yml b/.github/workflows/sdk-pull-request.yml similarity index 99% rename from .github/workflows/pull-request.yml rename to .github/workflows/sdk-pull-request.yml index 8512f20195..fc49b8311b 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/sdk-pull-request.yml @@ -1,4 +1,4 @@ -name: "Pull Request" +name: SDK - Pull Request on: push: diff --git a/.github/workflows/pypi-release.yml b/.github/workflows/sdk-pypi-release.yml similarity index 98% rename from .github/workflows/pypi-release.yml rename to .github/workflows/sdk-pypi-release.yml index 2c8ae5e659..4d9c91d690 100644 --- a/.github/workflows/pypi-release.yml +++ b/.github/workflows/sdk-pypi-release.yml @@ -1,4 +1,4 @@ -name: PyPI release +name: SDK - PyPI release on: release: diff --git a/.github/workflows/refresh_aws_services_regions.yml b/.github/workflows/sdk-refresh-aws-services-regions.yml similarity index 98% rename from .github/workflows/refresh_aws_services_regions.yml rename to .github/workflows/sdk-refresh-aws-services-regions.yml index 548f8d5600..bf7af302e2 100644 --- a/.github/workflows/refresh_aws_services_regions.yml +++ b/.github/workflows/sdk-refresh-aws-services-regions.yml @@ -1,6 +1,6 @@ # This is a basic workflow to help you get started with Actions -name: Refresh regions of AWS services +name: SDK - Refresh AWS services' regions on: schedule: diff --git a/.github/workflows/ui-codeql.yml b/.github/workflows/ui-codeql.yml index 2765921cf6..30586f43e7 100644 --- a/.github/workflows/ui-codeql.yml +++ b/.github/workflows/ui-codeql.yml @@ -9,7 +9,7 @@ # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # -name: "UI - CodeQL" +name: UI - CodeQL on: push: diff --git a/.github/workflows/ui-checks.yml b/.github/workflows/ui-pull-request.yml similarity index 93% rename from .github/workflows/ui-checks.yml rename to .github/workflows/ui-pull-request.yml index f0556fc61f..28e5d32e0a 100644 --- a/.github/workflows/ui-checks.yml +++ b/.github/workflows/ui-pull-request.yml @@ -1,4 +1,4 @@ -name: "UI - Pull Request" +name: UI - Pull Request on: pull_request: @@ -31,4 +31,4 @@ jobs: run: npm run healthcheck - name: Build the application working-directory: ./ui - run: npm run build \ No newline at end of file + run: npm run build From cdd044d12071c6af38079fe02bb10ec5e89b7d2a Mon Sep 17 00:00:00 2001 From: Pepe Fagoaga Date: Mon, 9 Dec 2024 14:15:03 +0100 Subject: [PATCH 20/67] chore(dependabot): Update for UI and v4 (#6062) --- .github/dependabot.yml | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 7b06e0da7a..4be019726a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,6 +5,7 @@ version: 2 updates: + # v5 - package-ecosystem: "pip" directory: "/" schedule: @@ -14,6 +15,7 @@ updates: labels: - "dependencies" - "pip" + - package-ecosystem: "github-actions" directory: "/" schedule: @@ -23,7 +25,41 @@ updates: labels: - "dependencies" - "github_actions" + + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + target-branch: master + labels: + - "dependencies" + - "npm" + + # v4.6 + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + target-branch: v4.6 + labels: + - "dependencies" + - "pip" + - "v4" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + target-branch: v3 + labels: + - "dependencies" + - "github_actions" + - "v4" + # v3 - package-ecosystem: "pip" directory: "/" schedule: @@ -34,6 +70,7 @@ updates: - "dependencies" - "pip" - "v3" + - package-ecosystem: "github-actions" directory: "/" schedule: From 4b41bd6adf45db7974064bf2850b06932825f7a8 Mon Sep 17 00:00:00 2001 From: Pepe Fagoaga Date: Mon, 9 Dec 2024 14:23:06 +0100 Subject: [PATCH 21/67] chore(containers): support for v4.6 branch (#6063) Co-authored-by: MrCloudSec --- .github/dependabot.yml | 12 ++++++------ .github/workflows/sdk-build-lint-push-containers.yml | 4 ++++ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 4be019726a..f002e143dd 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -15,7 +15,7 @@ updates: labels: - "dependencies" - "pip" - + - package-ecosystem: "github-actions" directory: "/" schedule: @@ -25,7 +25,7 @@ updates: labels: - "dependencies" - "github_actions" - + - package-ecosystem: "npm" directory: "/" schedule: @@ -35,7 +35,7 @@ updates: labels: - "dependencies" - "npm" - + # v4.6 - package-ecosystem: "pip" directory: "/" @@ -47,13 +47,13 @@ updates: - "dependencies" - "pip" - "v4" - + - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" open-pull-requests-limit: 10 - target-branch: v3 + target-branch: v4.6 labels: - "dependencies" - "github_actions" @@ -70,7 +70,7 @@ updates: - "dependencies" - "pip" - "v3" - + - package-ecosystem: "github-actions" directory: "/" schedule: diff --git a/.github/workflows/sdk-build-lint-push-containers.yml b/.github/workflows/sdk-build-lint-push-containers.yml index b502af0ba4..98460b3214 100644 --- a/.github/workflows/sdk-build-lint-push-containers.yml +++ b/.github/workflows/sdk-build-lint-push-containers.yml @@ -3,7 +3,11 @@ name: SDK - Build and Push containers on: push: branches: + # For `v3-latest` - "v3" + # For `v4-latest` + - "v4.6" + # For `latest` - "master" paths-ignore: - ".github/**" From d454427b8b5e6af55ae61a1a6fdf84d5b6cda1b9 Mon Sep 17 00:00:00 2001 From: Pepe Fagoaga Date: Mon, 9 Dec 2024 15:13:20 +0100 Subject: [PATCH 22/67] fix(backport): remove v from branch prefix (#6081) --- .github/workflows/backport.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 6639b526fb..f96db7d656 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -6,7 +6,9 @@ on: types: ['labeled', 'closed'] env: - BACKPORT_LABEL_PREFIX: backport-to-v + # The prefix of the label that triggers the backport must not contain the branch name + # so, for example, if the branch is 'master', the label should be 'backport-to-' + BACKPORT_LABEL_PREFIX: backport-to- BACKPORT_LABEL_IGNORE: was-backported jobs: @@ -25,8 +27,8 @@ jobs: with: allow_failure: true prefix_mode: true - one_of: ${{ env.BACKPORT_LABEL_PREFIX}} - none_of: ${{ env.BACKPORT_LABEL_IGNORE}} + one_of: ${{ env.BACKPORT_LABEL_PREFIX }} + none_of: ${{ env.BACKPORT_LABEL_IGNORE }} repo_token: ${{ secrets.GITHUB_TOKEN }} - name: Backport Action @@ -34,7 +36,7 @@ jobs: uses: sorenlouv/backport-github-action@v9.5.1 with: github_token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} - auto_backport_label_prefix: ${{ env.BACKPORT_LABEL_PREFIX}} + auto_backport_label_prefix: ${{ env.BACKPORT_LABEL_PREFIX }} - name: Info log if: ${{ success() && steps.preview_label_check.outputs.label_check == 'success' }} From a3051bc4e388bd9aaf0391da901075e4745e4b89 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 10:14:08 -0400 Subject: [PATCH 23/67] chore(deps-dev): bump mkdocs-material from 9.5.47 to 9.5.48 (#6073) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index cb022c0885..606d70487d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2583,13 +2583,13 @@ dev = ["click", "codecov", "mkdocs-gen-files", "mkdocs-git-authors-plugin", "mkd [[package]] name = "mkdocs-material" -version = "9.5.47" +version = "9.5.48" description = "Documentation that simply works" optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_material-9.5.47-py3-none-any.whl", hash = "sha256:53fb9c9624e7865da6ec807d116cd7be24b3cb36ab31b1d1d1a9af58c56009a2"}, - {file = "mkdocs_material-9.5.47.tar.gz", hash = "sha256:fc3b7a8e00ad896660bd3a5cc12ca0cb28bdc2bcbe2a946b5714c23ac91b0ede"}, + {file = "mkdocs_material-9.5.48-py3-none-any.whl", hash = "sha256:b695c998f4b939ce748adbc0d3bff73fa886a670ece948cf27818fa115dc16f8"}, + {file = "mkdocs_material-9.5.48.tar.gz", hash = "sha256:a582531e8b34f4c7ed38c29d5c44763053832cf2a32f7409567e0c74749a47db"}, ] [package.dependencies] @@ -5194,4 +5194,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "af11b4809a220ca4704bdff59e55883d4d87661a2fcb301f073ab807a8c29193" +content-hash = "bfbdf92461efe2bc2ed13fd0650de518055306ffd96a1235667f96eef0c14dc5" diff --git a/pyproject.toml b/pyproject.toml index d6329e51ab..204ede6b38 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -100,7 +100,7 @@ optional = true [tool.poetry.group.docs.dependencies] mkdocs = "1.6.1" mkdocs-git-revision-date-localized-plugin = "1.3.0" -mkdocs-material = "9.5.47" +mkdocs-material = "9.5.48" mkdocs-material-extensions = "1.3.1" [tool.poetry.scripts] From 2ea5851b6768abc5c591417c8d313bb614d5a446 Mon Sep 17 00:00:00 2001 From: Sergio Garcia Date: Mon, 9 Dec 2024 10:34:02 -0400 Subject: [PATCH 24/67] docs(api): add commands to run API scheduler (#6085) --- README.md | 13 +++++++++++++ docs/index.md | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/README.md b/README.md index 2e1a7cfeb5..145ba4048b 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,19 @@ cd src/backend python -m celery -A config.celery worker -l info -E ``` +**Commands to run the API Scheduler** + +``` console +git clone https://github.com/prowler-cloud/prowler +cd prowler/api +poetry install +poetry shell +set -a +source .env +cd src/backend +python -m celery -A config.celery beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler +``` + **Commands to run the UI** ``` console diff --git a/docs/index.md b/docs/index.md index 8dc5f08ffc..3ab1b4cfd6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -98,6 +98,19 @@ Prowler App can be installed in different ways, depending on your environment: python -m celery -A config.celery worker -l info -E ``` + _Commands to run the API Scheduler_: + + ``` bash + git clone https://github.com/prowler-cloud/prowler \ + cd prowler/api \ + poetry install \ + poetry shell \ + set -a \ + source .env \ + cd src/backend \ + python -m celery -A config.celery beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler + ``` + _Commands to run the UI_: ``` bash From f1efc1456dc97eeee8817f9a8aa083a510defeb4 Mon Sep 17 00:00:00 2001 From: Sergio Garcia Date: Mon, 9 Dec 2024 10:46:28 -0400 Subject: [PATCH 25/67] chore(dependabot): change interval of PRs (#6086) --- .github/dependabot.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f002e143dd..facc227ae6 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -40,7 +40,7 @@ updates: - package-ecosystem: "pip" directory: "/" schedule: - interval: "daily" + interval: "weekly" open-pull-requests-limit: 10 target-branch: v4.6 labels: @@ -51,7 +51,7 @@ updates: - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "daily" + interval: "weekly" open-pull-requests-limit: 10 target-branch: v4.6 labels: @@ -63,7 +63,7 @@ updates: - package-ecosystem: "pip" directory: "/" schedule: - interval: "daily" + interval: "monthly" open-pull-requests-limit: 10 target-branch: v3 labels: @@ -74,7 +74,7 @@ updates: - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "daily" + interval: "monthly" open-pull-requests-limit: 10 target-branch: v3 labels: From f8b4427505ad933432ce336860fe93475426e33b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 11:10:41 -0400 Subject: [PATCH 26/67] chore(deps-dev): bump vulture from 2.13 to 2.14 (#6068) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index 606d70487d..8fc3731738 100644 --- a/poetry.lock +++ b/poetry.lock @@ -4888,13 +4888,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "vulture" -version = "2.13" +version = "2.14" description = "Find dead code" optional = false python-versions = ">=3.8" files = [ - {file = "vulture-2.13-py2.py3-none-any.whl", hash = "sha256:34793ba60488e7cccbecdef3a7fe151656372ef94fdac9fe004c52a4000a6d44"}, - {file = "vulture-2.13.tar.gz", hash = "sha256:78248bf58f5eaffcc2ade306141ead73f437339950f80045dce7f8b078e5a1aa"}, + {file = "vulture-2.14-py2.py3-none-any.whl", hash = "sha256:d9a90dba89607489548a49d557f8bac8112bd25d3cbc8aeef23e860811bd5ed9"}, + {file = "vulture-2.14.tar.gz", hash = "sha256:cb8277902a1138deeab796ec5bef7076a6e0248ca3607a3f3dee0b6d9e9b8415"}, ] [package.dependencies] @@ -5194,4 +5194,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "bfbdf92461efe2bc2ed13fd0650de518055306ffd96a1235667f96eef0c14dc5" +content-hash = "7bcc5f7dd3503619d759c2c02c3ac66124db2f1607c234a1da30becf372a9c7d" diff --git a/pyproject.toml b/pyproject.toml index 204ede6b38..6ad437f8c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,7 +92,7 @@ pytest-env = "1.1.5" pytest-randomly = "3.16.0" pytest-xdist = "3.6.1" safety = "3.2.9" -vulture = "2.13" +vulture = "2.14" [tool.poetry.group.docs] optional = true From 9a55632d8e88a301d5fc0e717924c39796b3fc98 Mon Sep 17 00:00:00 2001 From: Pepe Fagoaga Date: Mon, 9 Dec 2024 17:19:33 +0100 Subject: [PATCH 27/67] fix(backport): more than one backport tag is allowed (#6090) --- .github/workflows/backport.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index f96db7d656..d74b96f3f8 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -27,7 +27,7 @@ jobs: with: allow_failure: true prefix_mode: true - one_of: ${{ env.BACKPORT_LABEL_PREFIX }} + any_of: ${{ env.BACKPORT_LABEL_PREFIX }} none_of: ${{ env.BACKPORT_LABEL_IGNORE }} repo_token: ${{ secrets.GITHUB_TOKEN }} From 7ddd2c04c836a84a57c50eb27c2bc00a58946090 Mon Sep 17 00:00:00 2001 From: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com> Date: Mon, 9 Dec 2024 17:28:42 +0100 Subject: [PATCH 28/67] feat(awslambda): add new fixer `awslambda_function_not_publicly_accessible_fixer` (#5840) --- ...wslambda_function_inside_vpc.metadata.json | 2 +- ...s_cloudtrail_logging_enabled.metadata.json | 2 +- ..._function_no_secrets_in_code.metadata.json | 2 +- ...tion_no_secrets_in_variables.metadata.json | 2 +- ...tion_not_publicly_accessible.metadata.json | 2 +- ..._function_not_publicly_accessible_fixer.py | 61 ++++ ...bda_function_url_cors_policy.metadata.json | 2 +- ...wslambda_function_url_public.metadata.json | 2 +- ...ion_using_supported_runtimes.metadata.json | 2 +- ...lambda_function_vpc_multi_az.metadata.json | 2 +- ...tion_not_publicly_accessible_fixer_test.py | 268 ++++++++++++++++++ 11 files changed, 338 insertions(+), 9 deletions(-) create mode 100644 prowler/providers/aws/services/awslambda/awslambda_function_not_publicly_accessible/awslambda_function_not_publicly_accessible_fixer.py create mode 100644 tests/providers/aws/services/awslambda/awslambda_function_not_publicly_accessible/awslambda_function_not_publicly_accessible_fixer_test.py diff --git a/prowler/providers/aws/services/awslambda/awslambda_function_inside_vpc/awslambda_function_inside_vpc.metadata.json b/prowler/providers/aws/services/awslambda/awslambda_function_inside_vpc/awslambda_function_inside_vpc.metadata.json index ed9b02372e..b25df32ee0 100644 --- a/prowler/providers/aws/services/awslambda/awslambda_function_inside_vpc/awslambda_function_inside_vpc.metadata.json +++ b/prowler/providers/aws/services/awslambda/awslambda_function_inside_vpc/awslambda_function_inside_vpc.metadata.json @@ -3,7 +3,7 @@ "CheckID": "awslambda_function_inside_vpc", "CheckTitle": "Ensure AWS Lambda Functions Are Deployed Inside a VPC", "CheckType": [], - "ServiceName": "lambda", + "ServiceName": "awslambda", "SubServiceName": "", "ResourceIdTemplate": "arn:partition:lambda:region:account-id:function/function-name", "Severity": "low", diff --git a/prowler/providers/aws/services/awslambda/awslambda_function_invoke_api_operations_cloudtrail_logging_enabled/awslambda_function_invoke_api_operations_cloudtrail_logging_enabled.metadata.json b/prowler/providers/aws/services/awslambda/awslambda_function_invoke_api_operations_cloudtrail_logging_enabled/awslambda_function_invoke_api_operations_cloudtrail_logging_enabled.metadata.json index 1782488f9c..4354b042c3 100644 --- a/prowler/providers/aws/services/awslambda/awslambda_function_invoke_api_operations_cloudtrail_logging_enabled/awslambda_function_invoke_api_operations_cloudtrail_logging_enabled.metadata.json +++ b/prowler/providers/aws/services/awslambda/awslambda_function_invoke_api_operations_cloudtrail_logging_enabled/awslambda_function_invoke_api_operations_cloudtrail_logging_enabled.metadata.json @@ -3,7 +3,7 @@ "CheckID": "awslambda_function_invoke_api_operations_cloudtrail_logging_enabled", "CheckTitle": "Check if Lambda functions invoke API operations are being recorded by CloudTrail.", "CheckType": [], - "ServiceName": "lambda", + "ServiceName": "awslambda", "SubServiceName": "", "ResourceIdTemplate": "arn:partition:lambda:region:account-id:function/function-name", "Severity": "low", diff --git a/prowler/providers/aws/services/awslambda/awslambda_function_no_secrets_in_code/awslambda_function_no_secrets_in_code.metadata.json b/prowler/providers/aws/services/awslambda/awslambda_function_no_secrets_in_code/awslambda_function_no_secrets_in_code.metadata.json index 4fd1e9dc57..43cb61491d 100644 --- a/prowler/providers/aws/services/awslambda/awslambda_function_no_secrets_in_code/awslambda_function_no_secrets_in_code.metadata.json +++ b/prowler/providers/aws/services/awslambda/awslambda_function_no_secrets_in_code/awslambda_function_no_secrets_in_code.metadata.json @@ -3,7 +3,7 @@ "CheckID": "awslambda_function_no_secrets_in_code", "CheckTitle": "Find secrets in Lambda functions code.", "CheckType": [], - "ServiceName": "lambda", + "ServiceName": "awslambda", "SubServiceName": "", "ResourceIdTemplate": "arn:partition:lambda:region:account-id:function/function-name", "Severity": "critical", diff --git a/prowler/providers/aws/services/awslambda/awslambda_function_no_secrets_in_variables/awslambda_function_no_secrets_in_variables.metadata.json b/prowler/providers/aws/services/awslambda/awslambda_function_no_secrets_in_variables/awslambda_function_no_secrets_in_variables.metadata.json index 39365275f1..640d977fea 100644 --- a/prowler/providers/aws/services/awslambda/awslambda_function_no_secrets_in_variables/awslambda_function_no_secrets_in_variables.metadata.json +++ b/prowler/providers/aws/services/awslambda/awslambda_function_no_secrets_in_variables/awslambda_function_no_secrets_in_variables.metadata.json @@ -3,7 +3,7 @@ "CheckID": "awslambda_function_no_secrets_in_variables", "CheckTitle": "Find secrets in Lambda functions variables.", "CheckType": [], - "ServiceName": "lambda", + "ServiceName": "awslambda", "SubServiceName": "", "ResourceIdTemplate": "arn:partition:lambda:region:account-id:function/function-name", "Severity": "critical", diff --git a/prowler/providers/aws/services/awslambda/awslambda_function_not_publicly_accessible/awslambda_function_not_publicly_accessible.metadata.json b/prowler/providers/aws/services/awslambda/awslambda_function_not_publicly_accessible/awslambda_function_not_publicly_accessible.metadata.json index 8bff12f44d..874d8aca1a 100644 --- a/prowler/providers/aws/services/awslambda/awslambda_function_not_publicly_accessible/awslambda_function_not_publicly_accessible.metadata.json +++ b/prowler/providers/aws/services/awslambda/awslambda_function_not_publicly_accessible/awslambda_function_not_publicly_accessible.metadata.json @@ -3,7 +3,7 @@ "CheckID": "awslambda_function_not_publicly_accessible", "CheckTitle": "Check if Lambda functions have resource-based policy set as Public.", "CheckType": [], - "ServiceName": "lambda", + "ServiceName": "awslambda", "SubServiceName": "", "ResourceIdTemplate": "arn:partition:lambda:region:account-id:function/function-name", "Severity": "critical", diff --git a/prowler/providers/aws/services/awslambda/awslambda_function_not_publicly_accessible/awslambda_function_not_publicly_accessible_fixer.py b/prowler/providers/aws/services/awslambda/awslambda_function_not_publicly_accessible/awslambda_function_not_publicly_accessible_fixer.py new file mode 100644 index 0000000000..3a74fb17d6 --- /dev/null +++ b/prowler/providers/aws/services/awslambda/awslambda_function_not_publicly_accessible/awslambda_function_not_publicly_accessible_fixer.py @@ -0,0 +1,61 @@ +import json + +from prowler.lib.logger import logger +from prowler.providers.aws.services.awslambda.awslambda_client import awslambda_client + + +def fixer(resource_id: str, region: str) -> bool: + """ + Remove the Lambda function's resource-based policy to prevent public access and add a new permission for the account. + Specifically, this fixer deletes all permission statements associated with the Lambda function's policy and then adds a new permission. + Requires the lambda:RemovePermission and lambda:AddPermission permissions. + Permissions: + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "lambda:RemovePermission", + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": "lambda:AddPermission", + "Resource": "*" + } + ] + } + Args: + resource_id (str): The Lambda function name or ARN. + region (str): AWS region where the Lambda function exists. + Returns: + bool: True if the operation is successful (policy removed and permission added), False otherwise. + """ + try: + account_id = awslambda_client.audited_account + + regional_client = awslambda_client.regional_clients[region] + policy_response = regional_client.get_policy(FunctionName=resource_id) + policy = json.loads(policy_response.get("Policy")) + + for statement in policy.get("Statement", []): + statement_id = statement.get("Sid") + if statement_id: + regional_client.remove_permission( + FunctionName=resource_id, StatementId=statement_id + ) + + regional_client.add_permission( + FunctionName=resource_id, + StatementId="ProwlerFixerStatement", + Principal=account_id, + Action="lambda:InvokeFunction", + ) + + except Exception as error: + logger.error( + f"{region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return False + else: + return True diff --git a/prowler/providers/aws/services/awslambda/awslambda_function_url_cors_policy/awslambda_function_url_cors_policy.metadata.json b/prowler/providers/aws/services/awslambda/awslambda_function_url_cors_policy/awslambda_function_url_cors_policy.metadata.json index 582aed2f9d..77d0d0fa19 100644 --- a/prowler/providers/aws/services/awslambda/awslambda_function_url_cors_policy/awslambda_function_url_cors_policy.metadata.json +++ b/prowler/providers/aws/services/awslambda/awslambda_function_url_cors_policy/awslambda_function_url_cors_policy.metadata.json @@ -3,7 +3,7 @@ "CheckID": "awslambda_function_url_cors_policy", "CheckTitle": "Check Lambda Function URL CORS configuration.", "CheckType": [], - "ServiceName": "lambda", + "ServiceName": "awslambda", "SubServiceName": "", "ResourceIdTemplate": "arn:partition:lambda:region:account-id:function/function-name", "Severity": "medium", diff --git a/prowler/providers/aws/services/awslambda/awslambda_function_url_public/awslambda_function_url_public.metadata.json b/prowler/providers/aws/services/awslambda/awslambda_function_url_public/awslambda_function_url_public.metadata.json index 3e7f1a2eac..74fa75f1f7 100644 --- a/prowler/providers/aws/services/awslambda/awslambda_function_url_public/awslambda_function_url_public.metadata.json +++ b/prowler/providers/aws/services/awslambda/awslambda_function_url_public/awslambda_function_url_public.metadata.json @@ -3,7 +3,7 @@ "CheckID": "awslambda_function_url_public", "CheckTitle": "Check Public Lambda Function URL.", "CheckType": [], - "ServiceName": "lambda", + "ServiceName": "awslambda", "SubServiceName": "", "ResourceIdTemplate": "arn:partition:lambda:region:account-id:function/function-name", "Severity": "high", diff --git a/prowler/providers/aws/services/awslambda/awslambda_function_using_supported_runtimes/awslambda_function_using_supported_runtimes.metadata.json b/prowler/providers/aws/services/awslambda/awslambda_function_using_supported_runtimes/awslambda_function_using_supported_runtimes.metadata.json index b8a1c5a013..a3048e5b9e 100644 --- a/prowler/providers/aws/services/awslambda/awslambda_function_using_supported_runtimes/awslambda_function_using_supported_runtimes.metadata.json +++ b/prowler/providers/aws/services/awslambda/awslambda_function_using_supported_runtimes/awslambda_function_using_supported_runtimes.metadata.json @@ -3,7 +3,7 @@ "CheckID": "awslambda_function_using_supported_runtimes", "CheckTitle": "Find obsolete Lambda runtimes.", "CheckType": [], - "ServiceName": "lambda", + "ServiceName": "awslambda", "SubServiceName": "", "ResourceIdTemplate": "arn:partition:lambda:region:account-id:function/function-name", "Severity": "medium", diff --git a/prowler/providers/aws/services/awslambda/awslambda_function_vpc_multi_az/awslambda_function_vpc_multi_az.metadata.json b/prowler/providers/aws/services/awslambda/awslambda_function_vpc_multi_az/awslambda_function_vpc_multi_az.metadata.json index 312023e4da..6c6cfff74e 100644 --- a/prowler/providers/aws/services/awslambda/awslambda_function_vpc_multi_az/awslambda_function_vpc_multi_az.metadata.json +++ b/prowler/providers/aws/services/awslambda/awslambda_function_vpc_multi_az/awslambda_function_vpc_multi_az.metadata.json @@ -3,7 +3,7 @@ "CheckID": "awslambda_function_vpc_multi_az", "CheckTitle": "Check if AWS Lambda Function VPC is deployed Across Multiple Availability Zones", "CheckType": [], - "ServiceName": "lambda", + "ServiceName": "awslambda", "SubServiceName": "", "ResourceIdTemplate": "arn:partition:lambda:region:account-id:function/function-name", "Severity": "medium", diff --git a/tests/providers/aws/services/awslambda/awslambda_function_not_publicly_accessible/awslambda_function_not_publicly_accessible_fixer_test.py b/tests/providers/aws/services/awslambda/awslambda_function_not_publicly_accessible/awslambda_function_not_publicly_accessible_fixer_test.py new file mode 100644 index 0000000000..958a77c00e --- /dev/null +++ b/tests/providers/aws/services/awslambda/awslambda_function_not_publicly_accessible/awslambda_function_not_publicly_accessible_fixer_test.py @@ -0,0 +1,268 @@ +from json import dumps +from unittest import mock + +from boto3 import client +from moto import mock_aws + +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_EU_WEST_1, + set_mocked_aws_provider, +) + + +class Test_awslambda_function_not_publicly_accessible_fixer: + @mock_aws + def test_function_public(self): + # Create the mock IAM role + iam_client = client("iam", region_name=AWS_REGION_EU_WEST_1) + role_name = "test-role" + assume_role_policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"Service": "lambda.amazonaws.com"}, + "Action": "sts:AssumeRole", + } + ], + } + role_arn = iam_client.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(assume_role_policy_document), + )["Role"]["Arn"] + + function_name = "test-lambda" + + # Create the lambda function using boto3 client + lambda_client = client("lambda", region_name=AWS_REGION_EU_WEST_1) + lambda_client.create_function( + FunctionName=function_name, + Runtime="nodejs4.3", + Role=role_arn, + Handler="index.handler", + Code={"ZipFile": b"fileb://file-path/to/your-deployment-package.zip"}, + Description="Test Lambda function", + Timeout=3, + MemorySize=128, + Publish=True, + Tags={"tag1": "value1", "tag2": "value2"}, + )["FunctionArn"] + + # Attach the policy to the lambda function with a wildcard principal + lambda_client.add_permission( + FunctionName=function_name, + StatementId="public-access", + Action="lambda:InvokeFunction", + Principal="*", + ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + from prowler.providers.aws.services.awslambda.awslambda_service import Lambda + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.awslambda.awslambda_function_not_publicly_accessible.awslambda_function_not_publicly_accessible_fixer.awslambda_client", + new=Lambda(aws_provider), + ): + # Test Fixer + from prowler.providers.aws.services.awslambda.awslambda_function_not_publicly_accessible.awslambda_function_not_publicly_accessible_fixer import ( + fixer, + ) + + assert fixer(function_name, AWS_REGION_EU_WEST_1) + + @mock_aws + def test_function_public_with_source_account(self): + # Create the mock IAM role + iam_client = client("iam", region_name=AWS_REGION_EU_WEST_1) + role_name = "test-role" + assume_role_policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"Service": "lambda.amazonaws.com"}, + "Action": "sts:AssumeRole", + } + ], + } + role_arn = iam_client.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(assume_role_policy_document), + )["Role"]["Arn"] + + function_name = "test-lambda" + + # Create the lambda function using boto3 client + lambda_client = client("lambda", region_name=AWS_REGION_EU_WEST_1) + function_arn = lambda_client.create_function( + FunctionName=function_name, + Runtime="nodejs4.3", + Role=role_arn, + Handler="index.handler", + Code={"ZipFile": b"fileb://file-path/to/your-deployment-package.zip"}, + Description="Test Lambda function", + Timeout=3, + MemorySize=128, + Publish=True, + Tags={"tag1": "value1", "tag2": "value2"}, + )["FunctionArn"] + + # Attach the policy to the lambda function with a wildcard principal + lambda_client.add_permission( + FunctionName=function_name, + StatementId="public-access", + Action="lambda:InvokeFunction", + Principal="*", + SourceArn=function_arn, + SourceAccount=AWS_ACCOUNT_NUMBER, + ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + from prowler.providers.aws.services.awslambda.awslambda_service import Lambda + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.awslambda.awslambda_function_not_publicly_accessible.awslambda_function_not_publicly_accessible_fixer.awslambda_client", + new=Lambda(aws_provider), + ): + # Test Fixer + from prowler.providers.aws.services.awslambda.awslambda_function_not_publicly_accessible.awslambda_function_not_publicly_accessible_fixer import ( + fixer, + ) + + assert fixer(function_name, AWS_REGION_EU_WEST_1) + + @mock_aws + def test_function_not_public(self): + # Create the mock IAM role + iam_client = client("iam", region_name=AWS_REGION_EU_WEST_1) + role_name = "test-role" + assume_role_policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"Service": "lambda.amazonaws.com"}, + "Action": "sts:AssumeRole", + } + ], + } + role_arn = iam_client.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(assume_role_policy_document), + )["Role"]["Arn"] + + function_name = "test-lambda" + + # Create the lambda function using boto3 client + lambda_client = client("lambda", region_name=AWS_REGION_EU_WEST_1) + function_arn = lambda_client.create_function( + FunctionName=function_name, + Runtime="nodejs4.3", + Role=role_arn, + Handler="index.handler", + Code={"ZipFile": b"fileb://file-path/to/your-deployment-package.zip"}, + Description="Test Lambda function", + Timeout=3, + MemorySize=128, + Publish=True, + Tags={"tag1": "value1", "tag2": "value2"}, + )["FunctionArn"] + + # Attach the policy to the lambda function with a specific AWS account number as principal + lambda_client.add_permission( + FunctionName=function_name, + StatementId="public-access", + Action="lambda:InvokeFunction", + Principal=AWS_ACCOUNT_NUMBER, + SourceArn=function_arn, + ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + from prowler.providers.aws.services.awslambda.awslambda_service import Lambda + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.awslambda.awslambda_function_not_publicly_accessible.awslambda_function_not_publicly_accessible_fixer.awslambda_client", + new=Lambda(aws_provider), + ): + # Test Fixer + from prowler.providers.aws.services.awslambda.awslambda_function_not_publicly_accessible.awslambda_function_not_publicly_accessible_fixer import ( + fixer, + ) + + assert fixer(function_name, AWS_REGION_EU_WEST_1) + + @mock_aws + def test_function_public_error(self): + # Create the mock IAM role + iam_client = client("iam", region_name=AWS_REGION_EU_WEST_1) + role_name = "test-role" + assume_role_policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"Service": "lambda.amazonaws.com"}, + "Action": "sts:AssumeRole", + } + ], + } + role_arn = iam_client.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(assume_role_policy_document), + )["Role"]["Arn"] + + function_name = "test-lambda" + + # Create the lambda function using boto3 client + lambda_client = client("lambda", region_name=AWS_REGION_EU_WEST_1) + lambda_client.create_function( + FunctionName=function_name, + Runtime="nodejs4.3", + Role=role_arn, + Handler="index.handler", + Code={"ZipFile": b"fileb://file-path/to/your-deployment-package.zip"}, + Description="Test Lambda function", + Timeout=3, + MemorySize=128, + Publish=True, + Tags={"tag1": "value1", "tag2": "value2"}, + )["FunctionArn"] + + # Attach the policy to the lambda function with a wildcard principal + lambda_client.add_permission( + FunctionName=function_name, + StatementId="public-access", + Action="lambda:InvokeFunction", + Principal="*", + ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + from prowler.providers.aws.services.awslambda.awslambda_service import Lambda + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.awslambda.awslambda_function_not_publicly_accessible.awslambda_function_not_publicly_accessible_fixer.awslambda_client", + new=Lambda(aws_provider), + ): + # Test Fixer + from prowler.providers.aws.services.awslambda.awslambda_function_not_publicly_accessible.awslambda_function_not_publicly_accessible_fixer import ( + fixer, + ) + + assert not fixer("non-exsting", AWS_REGION_EU_WEST_1) From 5c2adf1e14953084c2bb144895e30b6f40870335 Mon Sep 17 00:00:00 2001 From: Mario Rodriguez Lopez <101330800+MarioRgzLpz@users.noreply.github.com> Date: Tue, 10 Dec 2024 08:51:19 +0100 Subject: [PATCH 29/67] docs(unitesting): Make some fixes to the documentation (#6102) --- docs/developer-guide/unit-testing.md | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/docs/developer-guide/unit-testing.md b/docs/developer-guide/unit-testing.md index 0f8c0cfe23..27c0110a0f 100644 --- a/docs/developer-guide/unit-testing.md +++ b/docs/developer-guide/unit-testing.md @@ -51,14 +51,14 @@ For the AWS provider we have ways to test a Prowler check based on the following We use and contribute to the [Moto](https://github.com/getmoto/moto) library which allows us to easily mock out tests based on AWS infrastructure. **It's awesome!** - AWS API calls covered by [Moto](https://github.com/getmoto/moto): - - Service tests with `@mock_` - - Checks tests with `@mock_` + - Service tests with `@mock_aws` + - Checks tests with `@mock_aws` - AWS API calls not covered by Moto: - Service test with `mock_make_api_call` - Checks tests with [MagicMock](https://docs.python.org/3/library/unittest.mock.html#unittest.mock.MagicMock) - AWS API calls partially covered by Moto: - - Service test with `@mock_` and `mock_make_api_call` - - Checks tests with `@mock_` and `mock_make_api_call` + - Service test with `@mock_aws` and `mock_make_api_call` + - Checks tests with `@mock_aws` and `mock_make_api_call` In the following section we are going to explain all of the above scenarios with examples. The main difference between those scenarios comes from if the [Moto](https://github.com/getmoto/moto) library covers the AWS API calls made by the service. You can check the covered API calls [here](https://github.com/getmoto/moto/blob/master/IMPLEMENTATION_COVERAGE.md). @@ -70,7 +70,7 @@ This section is going to be divided based on the API coverage of the [Moto](http #### API calls covered -If the [Moto](https://github.com/getmoto/moto) library covers the API calls we want to test, we can use the `@mock_` decorator. This will mocked out all the API calls made to AWS keeping the state within the code decorated, in this case the test function. +If the [Moto](https://github.com/getmoto/moto) library covers the API calls we want to test, we can use the `@mock_aws` decorator. This will mocked out all the API calls made to AWS keeping the state within the code decorated, in this case the test function. ```python # We need to import the unittest.mock to allow us to patch some objects @@ -80,8 +80,8 @@ from unittest import mock # Boto3 client and session to call the AWS APIs from boto3 import client, session -# Moto decorator for the IAM service we want to mock -from moto import mock_iam +# Moto decorator +from moto import mock_aws # Constants used AWS_ACCOUNT_NUMBER = "123456789012" @@ -91,10 +91,8 @@ AWS_REGION = "us-east-1" # We always name the test classes like Test_ class Test_iam_password_policy_uppercase: - # We include the Moto decorator for the service we want to use - # You can include more than one if two or more services are - # involved in test - @mock_iam + # We include the Moto decorator + @mock_aws # We name the tests with test___ def test_iam_password_policy_no_uppercase_flag(self): # First, we have to create an IAM client @@ -238,7 +236,7 @@ To do so, you need to mock the `botocore.client.BaseClient._make_api_call` funct import boto3 import botocore from unittest.mock import patch -from moto import mock_iam +from moto import mock_aws # Original botocore _make_api_call function orig = botocore.client.BaseClient._make_api_call From 38a0d2d740e886f905a047791b22274fe741d60d Mon Sep 17 00:00:00 2001 From: Sergio Garcia Date: Tue, 10 Dec 2024 03:55:41 -0400 Subject: [PATCH 30/67] fix(aws): set same severity for EC2 IMDSv2 checks (#6046) --- .../ec2_instance_account_imdsv2_enabled.metadata.json | 2 +- .../ec2_instance_imdsv2_enabled.metadata.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/prowler/providers/aws/services/ec2/ec2_instance_account_imdsv2_enabled/ec2_instance_account_imdsv2_enabled.metadata.json b/prowler/providers/aws/services/ec2/ec2_instance_account_imdsv2_enabled/ec2_instance_account_imdsv2_enabled.metadata.json index f5ff575139..f329b9df8b 100644 --- a/prowler/providers/aws/services/ec2/ec2_instance_account_imdsv2_enabled/ec2_instance_account_imdsv2_enabled.metadata.json +++ b/prowler/providers/aws/services/ec2/ec2_instance_account_imdsv2_enabled/ec2_instance_account_imdsv2_enabled.metadata.json @@ -8,7 +8,7 @@ "ServiceName": "ec2", "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id", - "Severity": "medium", + "Severity": "high", "ResourceType": "AwsEc2Instance", "Description": "Ensure Instance Metadata Service Version 2 (IMDSv2) is enforced for EC2 instances at the account level to protect against SSRF vulnerabilities.", "Risk": "EC2 instances that use IMDSv1 are vulnerable to SSRF attacks.", diff --git a/prowler/providers/aws/services/ec2/ec2_instance_imdsv2_enabled/ec2_instance_imdsv2_enabled.metadata.json b/prowler/providers/aws/services/ec2/ec2_instance_imdsv2_enabled/ec2_instance_imdsv2_enabled.metadata.json index a83a12a9d7..3f613e6421 100644 --- a/prowler/providers/aws/services/ec2/ec2_instance_imdsv2_enabled/ec2_instance_imdsv2_enabled.metadata.json +++ b/prowler/providers/aws/services/ec2/ec2_instance_imdsv2_enabled/ec2_instance_imdsv2_enabled.metadata.json @@ -8,7 +8,7 @@ "ServiceName": "ec2", "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", - "Severity": "medium", + "Severity": "high", "ResourceType": "AwsEc2Instance", "Description": "Check if EC2 Instance Metadata Service Version 2 (IMDSv2) is Enabled and Required.", "Risk": "Using IMDSv2 will protect from misconfiguration and SSRF vulnerabilities. IMDSv1 will not.", From e3013329eee6eef722db29d50669d1d6146dec27 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 09:26:36 -0400 Subject: [PATCH 31/67] chore(deps): bump botocore from 1.35.76 to 1.35.77 (#6098) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index 8fc3731738..59c7d54728 100644 --- a/poetry.lock +++ b/poetry.lock @@ -794,13 +794,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.35.76" +version = "1.35.77" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" files = [ - {file = "botocore-1.35.76-py3-none-any.whl", hash = "sha256:b4729d12d00267b3185628f83543917b6caae292385230ab464067621aa086af"}, - {file = "botocore-1.35.76.tar.gz", hash = "sha256:a75a42ae53395796b8300c5fefb2d65a8696dc40dc85e49cf3a769e0c0202b13"}, + {file = "botocore-1.35.77-py3-none-any.whl", hash = "sha256:3faa27d65841499762228902d7e215fa99a4c2fdc76c9113e1c3f339bdf685b8"}, + {file = "botocore-1.35.77.tar.gz", hash = "sha256:17b778016644e9342ca3ff2f430c1d1db0c6126e9b41a57cff52ac58e7a455e0"}, ] [package.dependencies] @@ -5194,4 +5194,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "7bcc5f7dd3503619d759c2c02c3ac66124db2f1607c234a1da30becf372a9c7d" +content-hash = "35d419f553bc11c3ca68b98fc9a01f8988aef3eb4dcf6bd2f18a2b13a119090c" diff --git a/pyproject.toml b/pyproject.toml index 6ad437f8c6..c585463110 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ azure-mgmt-subscription = "3.1.1" azure-mgmt-web = "7.3.1" azure-storage-blob = "12.24.0" boto3 = "1.35.76" -botocore = "1.35.76" +botocore = "1.35.77" colorama = "0.4.6" cryptography = "43.0.1" dash = "2.18.2" From 9312dda7c2d9ee003ab065a0fcfeace39ceaa70b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 10:37:04 -0400 Subject: [PATCH 32/67] chore(deps): bump microsoft-kiota-abstractions from 1.6.2 to 1.6.6 (#6038) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index 59c7d54728..56136eacac 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2403,13 +2403,13 @@ files = [ [[package]] name = "microsoft-kiota-abstractions" -version = "1.6.2" +version = "1.6.6" description = "Core abstractions for kiota generated libraries in Python" optional = false python-versions = "<4.0,>=3.8" files = [ - {file = "microsoft_kiota_abstractions-1.6.2-py3-none-any.whl", hash = "sha256:8c2c777748e80f17dba3809b5d149585d9918198f0f94125e87432f7165ba80e"}, - {file = "microsoft_kiota_abstractions-1.6.2.tar.gz", hash = "sha256:dec30f0fb427a051003e94b5c6fcf266f4702ecbd9d6961e3966124b9cbe41bf"}, + {file = "microsoft_kiota_abstractions-1.6.6-py3-none-any.whl", hash = "sha256:29071715baf0d604c381c5d17be47f35e6e63a441dcfb5e9141963406b469d50"}, + {file = "microsoft_kiota_abstractions-1.6.6.tar.gz", hash = "sha256:2554495b00c9c25b43f6964a71b65c89a277bd6b50f4d0028a7febcec6c4fd67"}, ] [package.dependencies] @@ -5194,4 +5194,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "35d419f553bc11c3ca68b98fc9a01f8988aef3eb4dcf6bd2f18a2b13a119090c" +content-hash = "7903f4b687eafef1667e5213be132073c2122346eea53546690b0284df080eae" diff --git a/pyproject.toml b/pyproject.toml index c585463110..ecf70b8bb1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,7 @@ google-api-python-client = "2.154.0" google-auth-httplib2 = ">=0.1,<0.3" jsonschema = "4.23.0" kubernetes = "31.0.0" -microsoft-kiota-abstractions = "1.6.2" +microsoft-kiota-abstractions = "1.6.6" msgraph-sdk = "1.12.0" numpy = "2.0.2" pandas = "2.2.3" From 48946fa4f7ce414dddcb1a4dbe039365a4cd2a9b Mon Sep 17 00:00:00 2001 From: Sergio Garcia Date: Tue, 10 Dec 2024 11:06:48 -0400 Subject: [PATCH 33/67] fix(gcp): make sure default project is active (#6097) --- prowler/providers/gcp/gcp_provider.py | 6 +- tests/providers/gcp/gcp_provider_test.py | 423 +++++++++++++++-------- 2 files changed, 282 insertions(+), 147 deletions(-) diff --git a/prowler/providers/gcp/gcp_provider.py b/prowler/providers/gcp/gcp_provider.py index 1365d3586d..4fff965212 100644 --- a/prowler/providers/gcp/gcp_provider.py +++ b/prowler/providers/gcp/gcp_provider.py @@ -181,8 +181,6 @@ class GcpProvider(Provider): message="No Project IDs can be accessed via Google Credentials.", ) if project_ids: - if self._default_project_id not in project_ids: - self._default_project_id = project_ids[0] for input_project in project_ids: for ( accessible_project_id, @@ -203,6 +201,10 @@ class GcpProvider(Provider): self._projects[project_id] = project self._project_ids.append(project_id) + # Change default project if not in active projects + if self._project_ids and self._default_project_id not in self._project_ids: + self._default_project_id = self._project_ids[0] + # Remove excluded projects if any input if excluded_project_ids: for excluded_project in excluded_project_ids: diff --git a/tests/providers/gcp/gcp_provider_test.py b/tests/providers/gcp/gcp_provider_test.py index 9c74821935..9916a6902b 100644 --- a/tests/providers/gcp/gcp_provider_test.py +++ b/tests/providers/gcp/gcp_provider_test.py @@ -51,18 +51,23 @@ class TestGCPProvider: execute=MagicMock(return_value={"projects": projects}) ) - with patch( - "prowler.providers.gcp.gcp_provider.GcpProvider.setup_session", - return_value=(None, "test-project"), - ), patch( - "prowler.providers.gcp.gcp_provider.GcpProvider.get_projects", - return_value=projects, - ), patch( - "prowler.providers.gcp.gcp_provider.GcpProvider.update_projects_with_organizations", - return_value=None, - ), patch( - "prowler.providers.gcp.gcp_provider.discovery.build", - return_value=mocked_service, + with ( + patch( + "prowler.providers.gcp.gcp_provider.GcpProvider.setup_session", + return_value=(None, "test-project"), + ), + patch( + "prowler.providers.gcp.gcp_provider.GcpProvider.get_projects", + return_value=projects, + ), + patch( + "prowler.providers.gcp.gcp_provider.GcpProvider.update_projects_with_organizations", + return_value=None, + ), + patch( + "prowler.providers.gcp.gcp_provider.discovery.build", + return_value=mocked_service, + ), ): gcp_provider = GcpProvider( project_id, @@ -119,18 +124,23 @@ class TestGCPProvider: mocked_service.projects.list.return_value = MagicMock( execute=MagicMock(return_value={"projects": projects}) ) - with patch( - "prowler.providers.gcp.gcp_provider.GcpProvider.setup_session", - return_value=(None, None), - ), patch( - "prowler.providers.gcp.gcp_provider.GcpProvider.get_projects", - return_value=projects, - ), patch( - "prowler.providers.gcp.gcp_provider.GcpProvider.update_projects_with_organizations", - return_value=None, - ), patch( - "prowler.providers.gcp.gcp_provider.discovery.build", - return_value=mocked_service, + with ( + patch( + "prowler.providers.gcp.gcp_provider.GcpProvider.setup_session", + return_value=(None, None), + ), + patch( + "prowler.providers.gcp.gcp_provider.GcpProvider.get_projects", + return_value=projects, + ), + patch( + "prowler.providers.gcp.gcp_provider.GcpProvider.update_projects_with_organizations", + return_value=None, + ), + patch( + "prowler.providers.gcp.gcp_provider.discovery.build", + return_value=mocked_service, + ), ): gcp_provider = GcpProvider( arguments.organization_id, @@ -193,21 +203,27 @@ class TestGCPProvider: mocked_service.projects.list.return_value = MagicMock( execute=MagicMock(return_value={"projects": projects}) ) - with patch( - "prowler.providers.gcp.gcp_provider.GcpProvider.get_projects", - return_value=projects, - ), patch( - "prowler.providers.gcp.gcp_provider.GcpProvider.update_projects_with_organizations", - return_value=None, - ), patch( - "os.path.abspath", - return_value="test_credentials_file", - ), patch( - "prowler.providers.gcp.gcp_provider.default", - return_value=(mocked_credentials, MagicMock()), - ), patch( - "prowler.providers.gcp.gcp_provider.discovery.build", - return_value=mocked_service, + with ( + patch( + "prowler.providers.gcp.gcp_provider.GcpProvider.get_projects", + return_value=projects, + ), + patch( + "prowler.providers.gcp.gcp_provider.GcpProvider.update_projects_with_organizations", + return_value=None, + ), + patch( + "os.path.abspath", + return_value="test_credentials_file", + ), + patch( + "prowler.providers.gcp.gcp_provider.default", + return_value=(mocked_credentials, MagicMock()), + ), + patch( + "prowler.providers.gcp.gcp_provider.discovery.build", + return_value=mocked_service, + ), ): gcp_provider = GcpProvider( arguments.organization_id, @@ -257,21 +273,27 @@ class TestGCPProvider: mocked_service.projects.list.return_value = MagicMock( execute=MagicMock(return_value={"projects": projects}) ) - with patch( - "prowler.providers.gcp.gcp_provider.GcpProvider.get_projects", - return_value=projects, - ), patch( - "prowler.providers.gcp.gcp_provider.GcpProvider.update_projects_with_organizations", - return_value=None, - ), patch( - "os.path.abspath", - return_value="test_credentials_file", - ), patch( - "prowler.providers.gcp.gcp_provider.default", - return_value=(mocked_credentials, MagicMock()), - ), patch( - "prowler.providers.gcp.gcp_provider.discovery.build", - return_value=mocked_service, + with ( + patch( + "prowler.providers.gcp.gcp_provider.GcpProvider.get_projects", + return_value=projects, + ), + patch( + "prowler.providers.gcp.gcp_provider.GcpProvider.update_projects_with_organizations", + return_value=None, + ), + patch( + "os.path.abspath", + return_value="test_credentials_file", + ), + patch( + "prowler.providers.gcp.gcp_provider.default", + return_value=(mocked_credentials, MagicMock()), + ), + patch( + "prowler.providers.gcp.gcp_provider.discovery.build", + return_value=mocked_service, + ), ): gcp_provider = GcpProvider( arguments.organization_id, @@ -334,21 +356,27 @@ class TestGCPProvider: mocked_service.projects.list.return_value = MagicMock( execute=MagicMock(return_value={"projects": projects}) ) - with patch( - "prowler.providers.gcp.gcp_provider.GcpProvider.get_projects", - return_value=projects, - ), patch( - "prowler.providers.gcp.gcp_provider.GcpProvider.update_projects_with_organizations", - return_value=None, - ), patch( - "os.path.abspath", - return_value="test_credentials_file", - ), patch( - "prowler.providers.gcp.gcp_provider.default", - return_value=(mocked_credentials, MagicMock()), - ), patch( - "prowler.providers.gcp.gcp_provider.discovery.build", - return_value=mocked_service, + with ( + patch( + "prowler.providers.gcp.gcp_provider.GcpProvider.get_projects", + return_value=projects, + ), + patch( + "prowler.providers.gcp.gcp_provider.GcpProvider.update_projects_with_organizations", + return_value=None, + ), + patch( + "os.path.abspath", + return_value="test_credentials_file", + ), + patch( + "prowler.providers.gcp.gcp_provider.default", + return_value=(mocked_credentials, MagicMock()), + ), + patch( + "prowler.providers.gcp.gcp_provider.discovery.build", + return_value=mocked_service, + ), ): gcp_provider = GcpProvider( arguments.organization_id, @@ -401,21 +429,27 @@ class TestGCPProvider: mocked_service.projects.list.return_value = MagicMock( execute=MagicMock(return_value={"projects": projects}) ) - with patch( - "prowler.providers.gcp.gcp_provider.GcpProvider.get_projects", - return_value=projects, - ), patch( - "prowler.providers.gcp.gcp_provider.GcpProvider.update_projects_with_organizations", - return_value=None, - ), patch( - "os.path.abspath", - return_value="test_credentials_file", - ), patch( - "prowler.providers.gcp.gcp_provider.default", - return_value=(mocked_credentials, MagicMock()), - ), patch( - "prowler.providers.gcp.gcp_provider.discovery.build", - return_value=mocked_service, + with ( + patch( + "prowler.providers.gcp.gcp_provider.GcpProvider.get_projects", + return_value=projects, + ), + patch( + "prowler.providers.gcp.gcp_provider.GcpProvider.update_projects_with_organizations", + return_value=None, + ), + patch( + "os.path.abspath", + return_value="test_credentials_file", + ), + patch( + "prowler.providers.gcp.gcp_provider.default", + return_value=(mocked_credentials, MagicMock()), + ), + patch( + "prowler.providers.gcp.gcp_provider.discovery.build", + return_value=mocked_service, + ), ): with pytest.raises(Exception) as e: GcpProvider( @@ -433,6 +467,81 @@ class TestGCPProvider: ) assert e.type == GCPNoAccesibleProjectsError + def test_setup_session_with_inactive_default_project(self): + mocked_credentials = MagicMock() + + mocked_credentials.refresh.return_value = None + mocked_credentials._service_account_email = "test-service-account-email" + + arguments = Namespace() + arguments.project_id = ["default_project", "active_project"] + arguments.excluded_project_id = [] + arguments.organization_id = None + arguments.list_project_id = False + arguments.credentials_file = "test_credentials_file" + arguments.impersonate_service_account = "" + arguments.config_file = default_config_file_path + arguments.fixer_config = default_fixer_config_file_path + + projects = { + "default_project": GCPProject( + number="55555555", + id="default_project", + name="default_project", + labels={"test": "value"}, + lifecycle_state="DELETE_REQUESTED", + ), + "active_project": GCPProject( + number="12345678", + id="active_project", + name="active_project", + labels={"test": "value"}, + lifecycle_state="ACTIVE", + ), + } + + mocked_service = MagicMock() + + mocked_service.projects.list.return_value = MagicMock( + execute=MagicMock(return_value={"projects": projects}) + ) + with ( + patch( + "prowler.providers.gcp.gcp_provider.GcpProvider.get_projects", + return_value=projects, + ), + patch( + "prowler.providers.gcp.gcp_provider.GcpProvider.update_projects_with_organizations", + return_value=None, + ), + patch( + "os.path.abspath", + return_value="test_credentials_file", + ), + patch( + "prowler.providers.gcp.gcp_provider.default", + return_value=(mocked_credentials, "default_project"), + ), + patch( + "prowler.providers.gcp.gcp_provider.discovery.build", + return_value=mocked_service, + ), + ): + gcp_provider = GcpProvider( + arguments.organization_id, + arguments.project_id, + arguments.excluded_project_id, + arguments.credentials_file, + arguments.impersonate_service_account, + arguments.list_project_id, + arguments.config_file, + arguments.fixer_config, + client_id=None, + client_secret=None, + refresh_token=None, + ) + assert gcp_provider.default_project_id == "active_project" + def test_print_credentials_default_options(self, capsys): mocked_credentials = MagicMock() @@ -464,21 +573,27 @@ class TestGCPProvider: mocked_service.projects.list.return_value = MagicMock( execute=MagicMock(return_value={"projects": projects}) ) - with patch( - "prowler.providers.gcp.gcp_provider.GcpProvider.get_projects", - return_value=projects, - ), patch( - "prowler.providers.gcp.gcp_provider.GcpProvider.update_projects_with_organizations", - return_value=None, - ), patch( - "os.path.abspath", - return_value="test_credentials_file", - ), patch( - "prowler.providers.gcp.gcp_provider.default", - return_value=(mocked_credentials, MagicMock()), - ), patch( - "prowler.providers.gcp.gcp_provider.discovery.build", - return_value=mocked_service, + with ( + patch( + "prowler.providers.gcp.gcp_provider.GcpProvider.get_projects", + return_value=projects, + ), + patch( + "prowler.providers.gcp.gcp_provider.GcpProvider.update_projects_with_organizations", + return_value=None, + ), + patch( + "os.path.abspath", + return_value="test_credentials_file", + ), + patch( + "prowler.providers.gcp.gcp_provider.default", + return_value=(mocked_credentials, MagicMock()), + ), + patch( + "prowler.providers.gcp.gcp_provider.discovery.build", + return_value=mocked_service, + ), ): gcp_provider = GcpProvider( arguments.organization_id, @@ -535,21 +650,27 @@ class TestGCPProvider: mocked_service.projects.list.return_value = MagicMock( execute=MagicMock(return_value={"projects": projects}) ) - with patch( - "prowler.providers.gcp.gcp_provider.GcpProvider.get_projects", - return_value=projects, - ), patch( - "prowler.providers.gcp.gcp_provider.GcpProvider.update_projects_with_organizations", - return_value=None, - ), patch( - "os.path.abspath", - return_value="test_credentials_file", - ), patch( - "prowler.providers.gcp.gcp_provider.default", - return_value=(mocked_credentials, MagicMock()), - ), patch( - "prowler.providers.gcp.gcp_provider.discovery.build", - return_value=mocked_service, + with ( + patch( + "prowler.providers.gcp.gcp_provider.GcpProvider.get_projects", + return_value=projects, + ), + patch( + "prowler.providers.gcp.gcp_provider.GcpProvider.update_projects_with_organizations", + return_value=None, + ), + patch( + "os.path.abspath", + return_value="test_credentials_file", + ), + patch( + "prowler.providers.gcp.gcp_provider.default", + return_value=(mocked_credentials, MagicMock()), + ), + patch( + "prowler.providers.gcp.gcp_provider.discovery.build", + return_value=mocked_service, + ), ): gcp_provider = GcpProvider( arguments.organization_id, @@ -614,21 +735,27 @@ class TestGCPProvider: execute=MagicMock(return_value={"projects": projects}) ) - with patch( - "prowler.providers.gcp.gcp_provider.GcpProvider.get_projects", - return_value=projects, - ), patch( - "prowler.providers.gcp.gcp_provider.GcpProvider.update_projects_with_organizations", - return_value=None, - ), patch( - "os.path.abspath", - return_value="test_credentials_file", - ), patch( - "prowler.providers.gcp.gcp_provider.default", - return_value=(mocked_credentials, MagicMock()), - ), patch( - "prowler.providers.gcp.gcp_provider.discovery.build", - return_value=mocked_service, + with ( + patch( + "prowler.providers.gcp.gcp_provider.GcpProvider.get_projects", + return_value=projects, + ), + patch( + "prowler.providers.gcp.gcp_provider.GcpProvider.update_projects_with_organizations", + return_value=None, + ), + patch( + "os.path.abspath", + return_value="test_credentials_file", + ), + patch( + "prowler.providers.gcp.gcp_provider.default", + return_value=(mocked_credentials, MagicMock()), + ), + patch( + "prowler.providers.gcp.gcp_provider.discovery.build", + return_value=mocked_service, + ), ): gcp_provider = GcpProvider( arguments.organization_id, @@ -698,12 +825,15 @@ class TestGCPProvider: execute=MagicMock(return_value={"projectId": project_id}) ) - with patch( - "prowler.providers.gcp.gcp_provider.GcpProvider.setup_session", - return_value=(None, project_id), - ), patch( - "prowler.providers.gcp.gcp_provider.discovery.build", - return_value=mocked_service, + with ( + patch( + "prowler.providers.gcp.gcp_provider.GcpProvider.setup_session", + return_value=(None, project_id), + ), + patch( + "prowler.providers.gcp.gcp_provider.discovery.build", + return_value=mocked_service, + ), ): output = GcpProvider.test_connection( client_id="test-client-id", @@ -730,16 +860,19 @@ class TestGCPProvider: execute=MagicMock(return_value={"projects": projects}) ) - with patch( - "prowler.providers.gcp.gcp_provider.GcpProvider.setup_session", - return_value=(None, "test-valid-project"), - ), patch( - "prowler.providers.gcp.gcp_provider.discovery.build", - return_value=mocked_service, - ), patch( - "prowler.providers.gcp.gcp_provider.GcpProvider.validate_project_id" - ) as mock_validate_project_id: - + with ( + patch( + "prowler.providers.gcp.gcp_provider.GcpProvider.setup_session", + return_value=(None, "test-valid-project"), + ), + patch( + "prowler.providers.gcp.gcp_provider.discovery.build", + return_value=mocked_service, + ), + patch( + "prowler.providers.gcp.gcp_provider.GcpProvider.validate_project_id" + ) as mock_validate_project_id, + ): mock_validate_project_id.side_effect = GCPInvalidProviderIdError( "Invalid project ID" ) From 21bbdccc41ac46d9fb32e85c673203ddd4f35233 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Jes=C3=BAs=20Pe=C3=B1a=20Rodr=C3=ADguez?= Date: Tue, 10 Dec 2024 16:27:52 +0100 Subject: [PATCH 34/67] fix(deploy): temporal fix for the alpine-python segmentation fault (#6109) --- Dockerfile | 2 +- docker-compose-dev.yml | 2 +- docker-compose.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 613bd790eb..d8472d3492 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.12-alpine +FROM python:3.12.8-alpine3.20 LABEL maintainer="https://github.com/prowler-cloud/prowler" diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 2c39e2d9d2..d49af9dfbb 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -37,7 +37,7 @@ services: - 3000:3000 postgres: - image: postgres:16.3-alpine + image: postgres:16.3-alpine3.20 hostname: "postgres-db" volumes: - ./_data/postgres:/var/lib/postgresql/data diff --git a/docker-compose.yml b/docker-compose.yml index 281c79ddf8..004e82e2fd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,7 +25,7 @@ services: - ${UI_PORT:-3000}:${UI_PORT:-3000} postgres: - image: postgres:16.3-alpine + image: postgres:16.3-alpine3.20 hostname: "postgres-db" volumes: - ./_data/postgres:/var/lib/postgresql/data From 6471d936bbcfffc63d0d2ba14545adea237a7829 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 11:42:40 -0400 Subject: [PATCH 35/67] chore(deps): bump msgraph-sdk from 1.12.0 to 1.14.0 (#5957) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index 56136eacac..0bb5c90d09 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2769,13 +2769,13 @@ dev = ["bumpver", "isort", "mypy", "pylint", "pytest", "yapf"] [[package]] name = "msgraph-sdk" -version = "1.12.0" +version = "1.14.0" description = "The Microsoft Graph Python SDK" optional = false python-versions = ">=3.8" files = [ - {file = "msgraph_sdk-1.12.0-py3-none-any.whl", hash = "sha256:ac298b546b240391b0e407379d039db32862a56d6fe15cf7c5f7e77631fc6771"}, - {file = "msgraph_sdk-1.12.0.tar.gz", hash = "sha256:fbb5a8a9f6eed89b496f207eb35b6b4cfc7fefa75608aeef07477a3b2276d4fa"}, + {file = "msgraph_sdk-1.14.0-py3-none-any.whl", hash = "sha256:1a2f327dc8fbe5a5e6d0d84cf71d605e7b118b3066b1e16f011ccd8fd927bb03"}, + {file = "msgraph_sdk-1.14.0.tar.gz", hash = "sha256:5bbda80941c5d1794682753b8b291bd2ebed719a43d6de949fd0cd613b6dfbbd"}, ] [package.dependencies] @@ -5194,4 +5194,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "7903f4b687eafef1667e5213be132073c2122346eea53546690b0284df080eae" +content-hash = "f1f58e6a345fb6e33aa3d47d2a65070491b0e1b58c9a92db786ce8ea8ebbb30f" diff --git a/pyproject.toml b/pyproject.toml index ecf70b8bb1..d62a0d941c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,7 @@ google-auth-httplib2 = ">=0.1,<0.3" jsonschema = "4.23.0" kubernetes = "31.0.0" microsoft-kiota-abstractions = "1.6.6" -msgraph-sdk = "1.12.0" +msgraph-sdk = "1.14.0" numpy = "2.0.2" pandas = "2.2.3" py-ocsf-models = "0.2.0" From f2f196cfcd94cba82ceb1c919d7aa44fc423bc04 Mon Sep 17 00:00:00 2001 From: Sergio Garcia Date: Tue, 10 Dec 2024 12:03:01 -0400 Subject: [PATCH 36/67] fix(aws): set IAM identity as resource in threat detection (#6048) --- ...cloudtrail_threat_detection_enumeration.py | 6 +- ...cloudtrail_threat_detection_llm_jacking.py | 6 +- ...l_threat_detection_privilege_escalation.py | 6 +- ...trail_threat_detection_enumeration_test.py | 85 +++++++++++-------- ...trail_threat_detection_llm_jacking_test.py | 85 +++++++++++-------- ...eat_detection_privilege_escalation_test.py | 85 +++++++++++-------- 6 files changed, 156 insertions(+), 117 deletions(-) diff --git a/prowler/providers/aws/services/cloudtrail/cloudtrail_threat_detection_enumeration/cloudtrail_threat_detection_enumeration.py b/prowler/providers/aws/services/cloudtrail/cloudtrail_threat_detection_enumeration/cloudtrail_threat_detection_enumeration.py index f1ff06da85..8b8dfeca40 100644 --- a/prowler/providers/aws/services/cloudtrail/cloudtrail_threat_detection_enumeration/cloudtrail_threat_detection_enumeration.py +++ b/prowler/providers/aws/services/cloudtrail/cloudtrail_threat_detection_enumeration/cloudtrail_threat_detection_enumeration.py @@ -67,10 +67,8 @@ class cloudtrail_threat_detection_enumeration(Check): found_potential_enumeration = True report = Check_Report_AWS(self.metadata()) report.region = cloudtrail_client.region - report.resource_id = cloudtrail_client.audited_account - report.resource_arn = cloudtrail_client._get_trail_arn_template( - cloudtrail_client.region - ) + report.resource_id = aws_identity_arn.split("/")[-1] + report.resource_arn = aws_identity_arn report.status = "FAIL" report.status_extended = f"Potential enumeration attack detected from AWS {aws_identity_type} {aws_identity_arn.split('/')[-1]} with an threshold of {identity_threshold}." findings.append(report) diff --git a/prowler/providers/aws/services/cloudtrail/cloudtrail_threat_detection_llm_jacking/cloudtrail_threat_detection_llm_jacking.py b/prowler/providers/aws/services/cloudtrail/cloudtrail_threat_detection_llm_jacking/cloudtrail_threat_detection_llm_jacking.py index 8686a9a2ed..0dd77c4463 100644 --- a/prowler/providers/aws/services/cloudtrail/cloudtrail_threat_detection_llm_jacking/cloudtrail_threat_detection_llm_jacking.py +++ b/prowler/providers/aws/services/cloudtrail/cloudtrail_threat_detection_llm_jacking/cloudtrail_threat_detection_llm_jacking.py @@ -67,10 +67,8 @@ class cloudtrail_threat_detection_llm_jacking(Check): found_potential_llm_jacking = True report = Check_Report_AWS(self.metadata()) report.region = cloudtrail_client.region - report.resource_id = cloudtrail_client.audited_account - report.resource_arn = cloudtrail_client._get_trail_arn_template( - cloudtrail_client.region - ) + report.resource_id = aws_identity_arn.split("/")[-1] + report.resource_arn = aws_identity_arn report.status = "FAIL" report.status_extended = f"Potential LLM Jacking attack detected from AWS {aws_identity_type} {aws_identity_arn.split('/')[-1]} with an threshold of {identity_threshold}." findings.append(report) diff --git a/prowler/providers/aws/services/cloudtrail/cloudtrail_threat_detection_privilege_escalation/cloudtrail_threat_detection_privilege_escalation.py b/prowler/providers/aws/services/cloudtrail/cloudtrail_threat_detection_privilege_escalation/cloudtrail_threat_detection_privilege_escalation.py index 2992400912..676dde026f 100644 --- a/prowler/providers/aws/services/cloudtrail/cloudtrail_threat_detection_privilege_escalation/cloudtrail_threat_detection_privilege_escalation.py +++ b/prowler/providers/aws/services/cloudtrail/cloudtrail_threat_detection_privilege_escalation/cloudtrail_threat_detection_privilege_escalation.py @@ -69,10 +69,8 @@ class cloudtrail_threat_detection_privilege_escalation(Check): found_potential_privilege_escalation = True report = Check_Report_AWS(self.metadata()) report.region = cloudtrail_client.region - report.resource_id = cloudtrail_client.audited_account - report.resource_arn = cloudtrail_client._get_trail_arn_template( - cloudtrail_client.region - ) + report.resource_id = aws_identity_arn.split("/")[-1] + report.resource_arn = aws_identity_arn report.status = "FAIL" report.status_extended = f"Potential privilege escalation attack detected from AWS {aws_identity_type} {aws_identity_arn.split('/')[-1]} with an threshold of {identity_threshold}." findings.append(report) diff --git a/tests/providers/aws/services/cloudtrail/cloudtrail_threat_detection_enumeration/cloudtrail_threat_detection_enumeration_test.py b/tests/providers/aws/services/cloudtrail/cloudtrail_threat_detection_enumeration/cloudtrail_threat_detection_enumeration_test.py index b38d448eac..b56252edb8 100644 --- a/tests/providers/aws/services/cloudtrail/cloudtrail_threat_detection_enumeration/cloudtrail_threat_detection_enumeration_test.py +++ b/tests/providers/aws/services/cloudtrail/cloudtrail_threat_detection_enumeration/cloudtrail_threat_detection_enumeration_test.py @@ -19,10 +19,10 @@ def mock_get_trail_arn_template(region=None, *_) -> str: def mock__get_lookup_events__(trail=None, event_name=None, minutes=None, *_) -> list: return [ { - "CloudTrailEvent": '{"eventName": "DescribeAccessEntry", "userIdentity": {"type": "IAMUser", "principalId": "EXAMPLE6E4XEGITWATV6R", "arn": "arn:aws:iam::123456789012:user/Mateo", "accountId": "123456789012", "accessKeyId": "AKIAIOSFODNN7EXAMPLE", "userName": "Mateo", "sessionContext": {"sessionIssuer": {}, "webIdFederationData": {}, "attributes": {"creationDate": "2023-07-19T21:11:57Z", "mfaAuthenticated": "false"}}}}' + "CloudTrailEvent": '{"eventName": "DescribeAccessEntry", "userIdentity": {"type": "IAMUser", "principalId": "EXAMPLE6E4XEGITWATV6R", "arn": "arn:aws:iam::123456789012:user/Attacker", "accountId": "123456789012", "accessKeyId": "AKIAIOSFODNN7EXAMPLE", "userName": "Attacker", "sessionContext": {"sessionIssuer": {}, "webIdFederationData": {}, "attributes": {"creationDate": "2023-07-19T21:11:57Z", "mfaAuthenticated": "false"}}}}' }, { - "CloudTrailEvent": '{"eventName": "DescribeAccountAttributes", "userIdentity": {"type": "IAMUser", "principalId": "EXAMPLE6E4XEGITWATV6R", "arn": "arn:aws:iam::123456789012:user/Mateo", "accountId": "123456789012", "accessKeyId": "AKIAIOSFODNN7EXAMPLE", "userName": "Mateo", "sessionContext": {"sessionIssuer": {}, "webIdFederationData": {}, "attributes": {"creationDate": "2023-07-19T21:11:57Z", "mfaAuthenticated": "false"}}}}' + "CloudTrailEvent": '{"eventName": "DescribeAccountAttributes", "userIdentity": {"type": "IAMUser", "principalId": "EXAMPLE6E4XEGITWATV6R", "arn": "arn:aws:iam::123456789012:user/Attacker", "accountId": "123456789012", "accessKeyId": "AKIAIOSFODNN7EXAMPLE", "userName": "Attacker", "sessionContext": {"sessionIssuer": {}, "webIdFederationData": {}, "attributes": {"creationDate": "2023-07-19T21:11:57Z", "mfaAuthenticated": "false"}}}}' }, ] @@ -50,12 +50,15 @@ class Test_cloudtrail_threat_detection_enumeration: cloudtrail_client.audited_account = AWS_ACCOUNT_NUMBER cloudtrail_client.region = AWS_REGION_US_EAST_1 - with mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_aws_provider(), - ), mock.patch( - "prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_enumeration.cloudtrail_threat_detection_enumeration.cloudtrail_client", - new=cloudtrail_client, + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider(), + ), + mock.patch( + "prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_enumeration.cloudtrail_threat_detection_enumeration.cloudtrail_client", + new=cloudtrail_client, + ), ): # Test Check from prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_enumeration.cloudtrail_threat_detection_enumeration import ( @@ -99,12 +102,15 @@ class Test_cloudtrail_threat_detection_enumeration: cloudtrail_client._lookup_events = mock__get_lookup_events__ cloudtrail_client._get_trail_arn_template = mock_get_trail_arn_template - with mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_aws_provider(), - ), mock.patch( - "prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_enumeration.cloudtrail_threat_detection_enumeration.cloudtrail_client", - new=cloudtrail_client, + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider(), + ), + mock.patch( + "prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_enumeration.cloudtrail_threat_detection_enumeration.cloudtrail_client", + new=cloudtrail_client, + ), ): # Test Check from prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_enumeration.cloudtrail_threat_detection_enumeration import ( @@ -148,12 +154,15 @@ class Test_cloudtrail_threat_detection_enumeration: cloudtrail_client._lookup_events = mock__get_lookup_events__ cloudtrail_client._get_trail_arn_template = mock_get_trail_arn_template - with mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_aws_provider(), - ), mock.patch( - "prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_enumeration.cloudtrail_threat_detection_enumeration.cloudtrail_client", - new=cloudtrail_client, + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider(), + ), + mock.patch( + "prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_enumeration.cloudtrail_threat_detection_enumeration.cloudtrail_client", + new=cloudtrail_client, + ), ): # Test Check from prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_enumeration.cloudtrail_threat_detection_enumeration import ( @@ -167,13 +176,13 @@ class Test_cloudtrail_threat_detection_enumeration: assert result[0].status == "FAIL" assert ( result[0].status_extended - == "Potential enumeration attack detected from AWS IAMUser Mateo with an threshold of 1.0." + == "Potential enumeration attack detected from AWS IAMUser Attacker with an threshold of 1.0." ) - assert result[0].resource_id == AWS_ACCOUNT_NUMBER + assert result[0].resource_id == "Attacker" assert result[0].region == AWS_REGION_US_EAST_1 assert ( result[0].resource_arn - == f"arn:aws:cloudtrail:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:trail" + == f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:user/Attacker" ) @mock_aws @@ -198,12 +207,15 @@ class Test_cloudtrail_threat_detection_enumeration: cloudtrail_client._lookup_events = mock__get_lookup_events__ cloudtrail_client._get_trail_arn_template = mock_get_trail_arn_template - with mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_aws_provider(), - ), mock.patch( - "prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_enumeration.cloudtrail_threat_detection_enumeration.cloudtrail_client", - new=cloudtrail_client, + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider(), + ), + mock.patch( + "prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_enumeration.cloudtrail_threat_detection_enumeration.cloudtrail_client", + new=cloudtrail_client, + ), ): # Test Check from prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_enumeration.cloudtrail_threat_detection_enumeration import ( @@ -247,12 +259,15 @@ class Test_cloudtrail_threat_detection_enumeration: cloudtrail_client._lookup_events = mock__get_lookup_events_aws_service__ cloudtrail_client._get_trail_arn_template = mock_get_trail_arn_template - with mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_aws_provider(), - ), mock.patch( - "prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_enumeration.cloudtrail_threat_detection_enumeration.cloudtrail_client", - new=cloudtrail_client, + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider(), + ), + mock.patch( + "prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_enumeration.cloudtrail_threat_detection_enumeration.cloudtrail_client", + new=cloudtrail_client, + ), ): # Test Check from prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_enumeration.cloudtrail_threat_detection_enumeration import ( diff --git a/tests/providers/aws/services/cloudtrail/cloudtrail_threat_detection_llm_jacking/cloudtrail_threat_detection_llm_jacking_test.py b/tests/providers/aws/services/cloudtrail/cloudtrail_threat_detection_llm_jacking/cloudtrail_threat_detection_llm_jacking_test.py index 06ad5e33cb..fa73c8a4a7 100644 --- a/tests/providers/aws/services/cloudtrail/cloudtrail_threat_detection_llm_jacking/cloudtrail_threat_detection_llm_jacking_test.py +++ b/tests/providers/aws/services/cloudtrail/cloudtrail_threat_detection_llm_jacking/cloudtrail_threat_detection_llm_jacking_test.py @@ -19,10 +19,10 @@ def mock_get_trail_arn_template(region=None, *_) -> str: def mock__get_lookup_events__(trail=None, event_name=None, minutes=None, *_) -> list: return [ { - "CloudTrailEvent": '{"eventName": "InvokeModel", "userIdentity": {"type": "IAMUser", "principalId": "EXAMPLE6E4XEGITWATV6R", "arn": "arn:aws:iam::123456789012:user/Mateo", "accountId": "123456789012", "accessKeyId": "AKIAIOSFODNN7EXAMPLE", "userName": "Mateo", "sessionContext": {"sessionIssuer": {}, "webIdFederationData": {}, "attributes": {"creationDate": "2023-07-19T21:11:57Z", "mfaAuthenticated": "false"}}}}' + "CloudTrailEvent": '{"eventName": "InvokeModel", "userIdentity": {"type": "IAMUser", "principalId": "EXAMPLE6E4XEGITWATV6R", "arn": "arn:aws:iam::123456789012:user/Attacker", "accountId": "123456789012", "accessKeyId": "AKIAIOSFODNN7EXAMPLE", "userName": "Attacker", "sessionContext": {"sessionIssuer": {}, "webIdFederationData": {}, "attributes": {"creationDate": "2023-07-19T21:11:57Z", "mfaAuthenticated": "false"}}}}' }, { - "CloudTrailEvent": '{"eventName": "InvokeModelWithResponseStream", "userIdentity": {"type": "IAMUser", "principalId": "EXAMPLE6E4XEGITWATV6R", "arn": "arn:aws:iam::123456789012:user/Mateo", "accountId": "123456789012", "accessKeyId": "AKIAIOSFODNN7EXAMPLE", "userName": "Mateo", "sessionContext": {"sessionIssuer": {}, "webIdFederationData": {}, "attributes": {"creationDate": "2023-07-19T21:11:57Z", "mfaAuthenticated": "false"}}}}' + "CloudTrailEvent": '{"eventName": "InvokeModelWithResponseStream", "userIdentity": {"type": "IAMUser", "principalId": "EXAMPLE6E4XEGITWATV6R", "arn": "arn:aws:iam::123456789012:user/Attacker", "accountId": "123456789012", "accessKeyId": "AKIAIOSFODNN7EXAMPLE", "userName": "Attacker", "sessionContext": {"sessionIssuer": {}, "webIdFederationData": {}, "attributes": {"creationDate": "2023-07-19T21:11:57Z", "mfaAuthenticated": "false"}}}}' }, ] @@ -50,12 +50,15 @@ class Test_cloudtrail_threat_detection_llm_jacking: cloudtrail_client.audited_account = AWS_ACCOUNT_NUMBER cloudtrail_client.region = AWS_REGION_US_EAST_1 - with mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_aws_provider(), - ), mock.patch( - "prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_llm_jacking.cloudtrail_threat_detection_llm_jacking.cloudtrail_client", - new=cloudtrail_client, + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider(), + ), + mock.patch( + "prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_llm_jacking.cloudtrail_threat_detection_llm_jacking.cloudtrail_client", + new=cloudtrail_client, + ), ): # Test Check from prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_llm_jacking.cloudtrail_threat_detection_llm_jacking import ( @@ -96,12 +99,15 @@ class Test_cloudtrail_threat_detection_llm_jacking: cloudtrail_client._lookup_events = mock__get_lookup_events__ cloudtrail_client._get_trail_arn_template = mock_get_trail_arn_template - with mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_aws_provider(), - ), mock.patch( - "prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_llm_jacking.cloudtrail_threat_detection_llm_jacking.cloudtrail_client", - new=cloudtrail_client, + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider(), + ), + mock.patch( + "prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_llm_jacking.cloudtrail_threat_detection_llm_jacking.cloudtrail_client", + new=cloudtrail_client, + ), ): # Test Check from prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_llm_jacking.cloudtrail_threat_detection_llm_jacking import ( @@ -145,12 +151,15 @@ class Test_cloudtrail_threat_detection_llm_jacking: cloudtrail_client._lookup_events = mock__get_lookup_events__ cloudtrail_client._get_trail_arn_template = mock_get_trail_arn_template - with mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_aws_provider(), - ), mock.patch( - "prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_llm_jacking.cloudtrail_threat_detection_llm_jacking.cloudtrail_client", - new=cloudtrail_client, + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider(), + ), + mock.patch( + "prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_llm_jacking.cloudtrail_threat_detection_llm_jacking.cloudtrail_client", + new=cloudtrail_client, + ), ): # Test Check from prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_llm_jacking.cloudtrail_threat_detection_llm_jacking import ( @@ -164,13 +173,13 @@ class Test_cloudtrail_threat_detection_llm_jacking: assert result[0].status == "FAIL" assert ( result[0].status_extended - == "Potential LLM Jacking attack detected from AWS IAMUser Mateo with an threshold of 1.0." + == "Potential LLM Jacking attack detected from AWS IAMUser Attacker with an threshold of 1.0." ) - assert result[0].resource_id == AWS_ACCOUNT_NUMBER + assert result[0].resource_id == "Attacker" assert result[0].region == AWS_REGION_US_EAST_1 assert ( result[0].resource_arn - == f"arn:aws:cloudtrail:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:trail" + == f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:user/Attacker" ) @mock_aws @@ -195,12 +204,15 @@ class Test_cloudtrail_threat_detection_llm_jacking: cloudtrail_client._lookup_events = mock__get_lookup_events__ cloudtrail_client._get_trail_arn_template = mock_get_trail_arn_template - with mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_aws_provider(), - ), mock.patch( - "prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_llm_jacking.cloudtrail_threat_detection_llm_jacking.cloudtrail_client", - new=cloudtrail_client, + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider(), + ), + mock.patch( + "prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_llm_jacking.cloudtrail_threat_detection_llm_jacking.cloudtrail_client", + new=cloudtrail_client, + ), ): # Test Check from prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_llm_jacking.cloudtrail_threat_detection_llm_jacking import ( @@ -244,12 +256,15 @@ class Test_cloudtrail_threat_detection_llm_jacking: cloudtrail_client._lookup_events = mock__get_lookup_events_aws_service__ cloudtrail_client._get_trail_arn_template = mock_get_trail_arn_template - with mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_aws_provider(), - ), mock.patch( - "prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_llm_jacking.cloudtrail_threat_detection_llm_jacking.cloudtrail_client", - new=cloudtrail_client, + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider(), + ), + mock.patch( + "prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_llm_jacking.cloudtrail_threat_detection_llm_jacking.cloudtrail_client", + new=cloudtrail_client, + ), ): # Test Check from prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_llm_jacking.cloudtrail_threat_detection_llm_jacking import ( diff --git a/tests/providers/aws/services/cloudtrail/cloudtrail_threat_detection_privilege_escalation/cloudtrail_threat_detection_privilege_escalation_test.py b/tests/providers/aws/services/cloudtrail/cloudtrail_threat_detection_privilege_escalation/cloudtrail_threat_detection_privilege_escalation_test.py index ba278db3cf..19a50172cb 100644 --- a/tests/providers/aws/services/cloudtrail/cloudtrail_threat_detection_privilege_escalation/cloudtrail_threat_detection_privilege_escalation_test.py +++ b/tests/providers/aws/services/cloudtrail/cloudtrail_threat_detection_privilege_escalation/cloudtrail_threat_detection_privilege_escalation_test.py @@ -19,10 +19,10 @@ def mock_get_trail_arn_template(region=None, *_) -> str: def mock__get_lookup_events__(trail=None, event_name=None, minutes=None, *_) -> list: return [ { - "CloudTrailEvent": '{"eventName": "CreateLoginProfile", "userIdentity": {"type": "IAMUser", "principalId": "EXAMPLE6E4XEGITWATV6R", "arn": "arn:aws:iam::123456789012:user/Mateo", "accountId": "123456789012", "accessKeyId": "AKIAIOSFODNN7EXAMPLE", "userName": "Mateo", "sessionContext": {"sessionIssuer": {}, "webIdFederationData": {}, "attributes": {"creationDate": "2023-07-19T21:11:57Z", "mfaAuthenticated": "false"}}}}' + "CloudTrailEvent": '{"eventName": "CreateLoginProfile", "userIdentity": {"type": "IAMUser", "principalId": "EXAMPLE6E4XEGITWATV6R", "arn": "arn:aws:iam::123456789012:user/Attacker", "accountId": "123456789012", "accessKeyId": "AKIAIOSFODNN7EXAMPLE", "userName": "Attacker", "sessionContext": {"sessionIssuer": {}, "webIdFederationData": {}, "attributes": {"creationDate": "2023-07-19T21:11:57Z", "mfaAuthenticated": "false"}}}}' }, { - "CloudTrailEvent": '{"eventName": "UpdateLoginProfile", "userIdentity": {"type": "IAMUser", "principalId": "EXAMPLE6E4XEGITWATV6R", "arn": "arn:aws:iam::123456789012:user/Mateo", "accountId": "123456789012", "accessKeyId": "AKIAIOSFODNN7EXAMPLE", "userName": "Mateo", "sessionContext": {"sessionIssuer": {}, "webIdFederationData": {}, "attributes": {"creationDate": "2023-07-19T21:11:57Z", "mfaAuthenticated": "false"}}}}' + "CloudTrailEvent": '{"eventName": "UpdateLoginProfile", "userIdentity": {"type": "IAMUser", "principalId": "EXAMPLE6E4XEGITWATV6R", "arn": "arn:aws:iam::123456789012:user/Attacker", "accountId": "123456789012", "accessKeyId": "AKIAIOSFODNN7EXAMPLE", "userName": "Attacker", "sessionContext": {"sessionIssuer": {}, "webIdFederationData": {}, "attributes": {"creationDate": "2023-07-19T21:11:57Z", "mfaAuthenticated": "false"}}}}' }, ] @@ -50,12 +50,15 @@ class Test_cloudtrail_threat_detection_privilege_escalation: cloudtrail_client.audited_account = AWS_ACCOUNT_NUMBER cloudtrail_client.region = AWS_REGION_US_EAST_1 - with mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_aws_provider(), - ), mock.patch( - "prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_privilege_escalation.cloudtrail_threat_detection_privilege_escalation.cloudtrail_client", - new=cloudtrail_client, + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider(), + ), + mock.patch( + "prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_privilege_escalation.cloudtrail_threat_detection_privilege_escalation.cloudtrail_client", + new=cloudtrail_client, + ), ): # Test Check from prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_privilege_escalation.cloudtrail_threat_detection_privilege_escalation import ( @@ -97,12 +100,15 @@ class Test_cloudtrail_threat_detection_privilege_escalation: cloudtrail_client._lookup_events = mock__get_lookup_events__ cloudtrail_client._get_trail_arn_template = mock_get_trail_arn_template - with mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_aws_provider(), - ), mock.patch( - "prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_privilege_escalation.cloudtrail_threat_detection_privilege_escalation.cloudtrail_client", - new=cloudtrail_client, + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider(), + ), + mock.patch( + "prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_privilege_escalation.cloudtrail_threat_detection_privilege_escalation.cloudtrail_client", + new=cloudtrail_client, + ), ): # Test Check from prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_privilege_escalation.cloudtrail_threat_detection_privilege_escalation import ( @@ -147,12 +153,15 @@ class Test_cloudtrail_threat_detection_privilege_escalation: cloudtrail_client._lookup_events = mock__get_lookup_events__ cloudtrail_client._get_trail_arn_template = mock_get_trail_arn_template - with mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_aws_provider(), - ), mock.patch( - "prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_privilege_escalation.cloudtrail_threat_detection_privilege_escalation.cloudtrail_client", - new=cloudtrail_client, + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider(), + ), + mock.patch( + "prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_privilege_escalation.cloudtrail_threat_detection_privilege_escalation.cloudtrail_client", + new=cloudtrail_client, + ), ): # Test Check from prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_privilege_escalation.cloudtrail_threat_detection_privilege_escalation import ( @@ -166,13 +175,13 @@ class Test_cloudtrail_threat_detection_privilege_escalation: assert result[0].status == "FAIL" assert ( result[0].status_extended - == "Potential privilege escalation attack detected from AWS IAMUser Mateo with an threshold of 1.0." + == "Potential privilege escalation attack detected from AWS IAMUser Attacker with an threshold of 1.0." ) - assert result[0].resource_id == AWS_ACCOUNT_NUMBER + assert result[0].resource_id == "Attacker" assert result[0].region == AWS_REGION_US_EAST_1 assert ( result[0].resource_arn - == f"arn:aws:cloudtrail:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:trail" + == f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:user/Attacker" ) @mock_aws @@ -197,12 +206,15 @@ class Test_cloudtrail_threat_detection_privilege_escalation: cloudtrail_client._lookup_events = mock__get_lookup_events__ cloudtrail_client._get_trail_arn_template = mock_get_trail_arn_template - with mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_aws_provider(), - ), mock.patch( - "prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_privilege_escalation.cloudtrail_threat_detection_privilege_escalation.cloudtrail_client", - new=cloudtrail_client, + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider(), + ), + mock.patch( + "prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_privilege_escalation.cloudtrail_threat_detection_privilege_escalation.cloudtrail_client", + new=cloudtrail_client, + ), ): # Test Check from prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_privilege_escalation.cloudtrail_threat_detection_privilege_escalation import ( @@ -247,12 +259,15 @@ class Test_cloudtrail_threat_detection_privilege_escalation: cloudtrail_client._lookup_events = mock__get_lookup_events_aws_service__ cloudtrail_client._get_trail_arn_template = mock_get_trail_arn_template - with mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_aws_provider(), - ), mock.patch( - "prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_privilege_escalation.cloudtrail_threat_detection_privilege_escalation.cloudtrail_client", - new=cloudtrail_client, + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider(), + ), + mock.patch( + "prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_privilege_escalation.cloudtrail_threat_detection_privilege_escalation.cloudtrail_client", + new=cloudtrail_client, + ), ): # Test Check from prowler.providers.aws.services.cloudtrail.cloudtrail_threat_detection_privilege_escalation.cloudtrail_threat_detection_privilege_escalation import ( From b984f0423a8ad80fffd5b9dc4853b034f4d5211e Mon Sep 17 00:00:00 2001 From: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com> Date: Tue, 10 Dec 2024 17:26:42 +0100 Subject: [PATCH 37/67] feat(sqs): add new fixer `sqs_queues_not_publicly_accessible_fixer` (#5911) Co-authored-by: Sergio Garcia --- ...qs_queues_not_publicly_accessible_fixer.py | 64 +++++++++++ ...eues_not_publicly_accessible_fixer_test.py | 106 ++++++++++++++++++ ...sqs_queues_not_publicly_accessible_test.py | 30 ++++- ...ues_server_side_encryption_enabled_test.py | 15 ++- 4 files changed, 206 insertions(+), 9 deletions(-) create mode 100644 prowler/providers/aws/services/sqs/sqs_queues_not_publicly_accessible/sqs_queues_not_publicly_accessible_fixer.py create mode 100644 tests/providers/aws/services/sqs/sqs_queues_not_publicly_accessible/sqs_queues_not_publicly_accessible_fixer_test.py diff --git a/prowler/providers/aws/services/sqs/sqs_queues_not_publicly_accessible/sqs_queues_not_publicly_accessible_fixer.py b/prowler/providers/aws/services/sqs/sqs_queues_not_publicly_accessible/sqs_queues_not_publicly_accessible_fixer.py new file mode 100644 index 0000000000..a8e0e489d4 --- /dev/null +++ b/prowler/providers/aws/services/sqs/sqs_queues_not_publicly_accessible/sqs_queues_not_publicly_accessible_fixer.py @@ -0,0 +1,64 @@ +import json + +from prowler.lib.logger import logger +from prowler.providers.aws.services.sqs.sqs_client import sqs_client + + +def fixer(resource_id: str, region: str) -> bool: + """ + Modify the SQS queue's resource-based policy to remove public access and replace with trusted account access. + Specifically, this fixer checks if any statement has a public Principal (e.g., "*" or "CanonicalUser") + and replaces it with the ARN of the trusted AWS account. + Requires the sqs:SetQueueAttributes permission. + Permissions: + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "sqs:SetQueueAttributes", + "Resource": "*" + } + ] + } + Args: + resource_id (str): The SQS queue name or ARN. + region (str): AWS region where the SQS queue exists. + Returns: + bool: True if the operation is successful (policy updated), False otherwise. + """ + try: + account_id = sqs_client.audited_account + audited_partition = sqs_client.audited_partition + + regional_client = sqs_client.regional_clients[region] + + queue_name = resource_id.split("/")[-1] + + trusted_policy = { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "ProwlerFixerStatement", + "Effect": "Allow", + "Principal": { + "AWS": account_id, + }, + "Action": "sqs:*", + "Resource": f"arn:{audited_partition}:sqs:{region}:{account_id}:{queue_name}", + } + ], + } + + regional_client.set_queue_attributes( + QueueUrl=resource_id, + Attributes={"Policy": json.dumps(trusted_policy)}, + ) + + except Exception as error: + logger.error( + f"{region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return False + else: + return True diff --git a/tests/providers/aws/services/sqs/sqs_queues_not_publicly_accessible/sqs_queues_not_publicly_accessible_fixer_test.py b/tests/providers/aws/services/sqs/sqs_queues_not_publicly_accessible/sqs_queues_not_publicly_accessible_fixer_test.py new file mode 100644 index 0000000000..8603573e3a --- /dev/null +++ b/tests/providers/aws/services/sqs/sqs_queues_not_publicly_accessible/sqs_queues_not_publicly_accessible_fixer_test.py @@ -0,0 +1,106 @@ +from json import dumps +from unittest import mock + +from boto3 import client +from moto import mock_aws + +from tests.providers.aws.utils import AWS_REGION_EU_WEST_1, set_mocked_aws_provider + + +class Test_sqs_queues_not_publicly_accessible_fixer: + @mock_aws + def test_queue_public(self): + sqs_client = client("sqs", region_name=AWS_REGION_EU_WEST_1) + + queue_url = sqs_client.create_queue(QueueName="test-queue")["QueueUrl"] + + sqs_client.set_queue_attributes( + QueueUrl=queue_url, + Attributes={ + "Policy": dumps({"Statement": [{"Effect": "Allow", "Principal": "*"}]}) + }, + ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + from prowler.providers.aws.services.sqs.sqs_service import SQS + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.sqs.sqs_queues_not_publicly_accessible.sqs_queues_not_publicly_accessible_fixer.sqs_client", + new=SQS(aws_provider), + ): + # Test Fixer + from prowler.providers.aws.services.sqs.sqs_queues_not_publicly_accessible.sqs_queues_not_publicly_accessible_fixer import ( + fixer, + ) + + assert fixer(queue_url, AWS_REGION_EU_WEST_1) + + @mock_aws + def test_queue_public_with_aws(self): + sqs_client = client("sqs", region_name=AWS_REGION_EU_WEST_1) + + queue_url = sqs_client.create_queue(QueueName="test-queue")["QueueUrl"] + + sqs_client.set_queue_attributes( + QueueUrl=queue_url, + Attributes={ + "Policy": dumps( + {"Statement": [{"Effect": "Allow", "Principal": {"AWS": "*"}}]} + ) + }, + ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + from prowler.providers.aws.services.sqs.sqs_service import SQS + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.sqs.sqs_queues_not_publicly_accessible.sqs_queues_not_publicly_accessible_fixer.sqs_client", + new=SQS(aws_provider), + ): + # Test Fixer + from prowler.providers.aws.services.sqs.sqs_queues_not_publicly_accessible.sqs_queues_not_publicly_accessible_fixer import ( + fixer, + ) + + assert fixer(queue_url, AWS_REGION_EU_WEST_1) + + @mock_aws + def test_queue_public_error(self): + sqs_client = client("sqs", region_name=AWS_REGION_EU_WEST_1) + + queue_url = sqs_client.create_queue(QueueName="test-queue")["QueueUrl"] + + sqs_client.set_queue_attributes( + QueueUrl=queue_url, + Attributes={ + "Policy": dumps( + {"Statement": [{"Effect": "Allow", "Principal": {"AWS": "*"}}]} + ) + }, + ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + from prowler.providers.aws.services.sqs.sqs_service import SQS + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.sqs.sqs_queues_not_publicly_accessible.sqs_queues_not_publicly_accessible_fixer.sqs_client", + new=SQS(aws_provider), + ): + # Test Fixer + from prowler.providers.aws.services.sqs.sqs_queues_not_publicly_accessible.sqs_queues_not_publicly_accessible_fixer import ( + fixer, + ) + + assert not fixer("queue_url_non_existing", AWS_REGION_EU_WEST_1) diff --git a/tests/providers/aws/services/sqs/sqs_queues_not_publicly_accessible/sqs_queues_not_publicly_accessible_test.py b/tests/providers/aws/services/sqs/sqs_queues_not_publicly_accessible/sqs_queues_not_publicly_accessible_test.py index 6f0c5197a5..b1d6ab3a78 100644 --- a/tests/providers/aws/services/sqs/sqs_queues_not_publicly_accessible/sqs_queues_not_publicly_accessible_test.py +++ b/tests/providers/aws/services/sqs/sqs_queues_not_publicly_accessible/sqs_queues_not_publicly_accessible_test.py @@ -110,7 +110,10 @@ class Test_sqs_queues_not_publicly_accessible: sqs_client.queues = [] with mock.patch( "prowler.providers.aws.services.sqs.sqs_service.SQS", - sqs_client, + new=sqs_client, + ), mock.patch( + "prowler.providers.aws.services.sqs.sqs_client.sqs_client", + new=sqs_client, ): from prowler.providers.aws.services.sqs.sqs_queues_not_publicly_accessible.sqs_queues_not_publicly_accessible import ( sqs_queues_not_publicly_accessible, @@ -167,7 +170,10 @@ class Test_sqs_queues_not_publicly_accessible: ) with mock.patch( "prowler.providers.aws.services.sqs.sqs_service.SQS", - sqs_client, + new=sqs_client, + ), mock.patch( + "prowler.providers.aws.services.sqs.sqs_client.sqs_client", + new=sqs_client, ): from prowler.providers.aws.services.sqs.sqs_queues_not_publicly_accessible.sqs_queues_not_publicly_accessible import ( sqs_queues_not_publicly_accessible, @@ -201,7 +207,10 @@ class Test_sqs_queues_not_publicly_accessible: ) with mock.patch( "prowler.providers.aws.services.sqs.sqs_service.SQS", - sqs_client, + new=sqs_client, + ), mock.patch( + "prowler.providers.aws.services.sqs.sqs_client.sqs_client", + new=sqs_client, ): from prowler.providers.aws.services.sqs.sqs_queues_not_publicly_accessible.sqs_queues_not_publicly_accessible import ( sqs_queues_not_publicly_accessible, @@ -235,7 +244,10 @@ class Test_sqs_queues_not_publicly_accessible: ) with mock.patch( "prowler.providers.aws.services.sqs.sqs_service.SQS", - sqs_client, + new=sqs_client, + ), mock.patch( + "prowler.providers.aws.services.sqs.sqs_client.sqs_client", + new=sqs_client, ): from prowler.providers.aws.services.sqs.sqs_queues_not_publicly_accessible.sqs_queues_not_publicly_accessible import ( sqs_queues_not_publicly_accessible, @@ -269,7 +281,10 @@ class Test_sqs_queues_not_publicly_accessible: ) with mock.patch( "prowler.providers.aws.services.sqs.sqs_service.SQS", - sqs_client, + new=sqs_client, + ), mock.patch( + "prowler.providers.aws.services.sqs.sqs_client.sqs_client", + new=sqs_client, ): from prowler.providers.aws.services.sqs.sqs_queues_not_publicly_accessible.sqs_queues_not_publicly_accessible import ( sqs_queues_not_publicly_accessible, @@ -303,7 +318,10 @@ class Test_sqs_queues_not_publicly_accessible: ) with mock.patch( "prowler.providers.aws.services.sqs.sqs_service.SQS", - sqs_client, + new=sqs_client, + ), mock.patch( + "prowler.providers.aws.services.sqs.sqs_client.sqs_client", + new=sqs_client, ): from prowler.providers.aws.services.sqs.sqs_queues_not_publicly_accessible.sqs_queues_not_publicly_accessible import ( sqs_queues_not_publicly_accessible, diff --git a/tests/providers/aws/services/sqs/sqs_queues_server_side_encryption_enabled/sqs_queues_server_side_encryption_enabled_test.py b/tests/providers/aws/services/sqs/sqs_queues_server_side_encryption_enabled/sqs_queues_server_side_encryption_enabled_test.py index e24b28b475..29d7f5ed05 100644 --- a/tests/providers/aws/services/sqs/sqs_queues_server_side_encryption_enabled/sqs_queues_server_side_encryption_enabled_test.py +++ b/tests/providers/aws/services/sqs/sqs_queues_server_side_encryption_enabled/sqs_queues_server_side_encryption_enabled_test.py @@ -18,7 +18,10 @@ class Test_sqs_queues_server_side_encryption_enabled: sqs_client.queues = [] with mock.patch( "prowler.providers.aws.services.sqs.sqs_service.SQS", - sqs_client, + new=sqs_client, + ), mock.patch( + "prowler.providers.aws.services.sqs.sqs_client.sqs_client", + new=sqs_client, ): from prowler.providers.aws.services.sqs.sqs_queues_server_side_encryption_enabled.sqs_queues_server_side_encryption_enabled import ( sqs_queues_server_side_encryption_enabled, @@ -42,7 +45,10 @@ class Test_sqs_queues_server_side_encryption_enabled: ) with mock.patch( "prowler.providers.aws.services.sqs.sqs_service.SQS", - sqs_client, + new=sqs_client, + ), mock.patch( + "prowler.providers.aws.services.sqs.sqs_client.sqs_client", + new=sqs_client, ): from prowler.providers.aws.services.sqs.sqs_queues_server_side_encryption_enabled.sqs_queues_server_side_encryption_enabled import ( sqs_queues_server_side_encryption_enabled, @@ -72,7 +78,10 @@ class Test_sqs_queues_server_side_encryption_enabled: ) with mock.patch( "prowler.providers.aws.services.sqs.sqs_service.SQS", - sqs_client, + new=sqs_client, + ), mock.patch( + "prowler.providers.aws.services.sqs.sqs_client.sqs_client", + new=sqs_client, ): from prowler.providers.aws.services.sqs.sqs_queues_server_side_encryption_enabled.sqs_queues_server_side_encryption_enabled import ( sqs_queues_server_side_encryption_enabled, From 1ae3f89aabc0149b22e872ee684c764b491f6efd Mon Sep 17 00:00:00 2001 From: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com> Date: Tue, 10 Dec 2024 18:42:13 +0100 Subject: [PATCH 38/67] fix(aws): check AWS Owned keys in `firehose_stream_encrypted_at_rest` (#6108) --- .../firehose_stream_encrypted_at_rest.py | 5 +- .../firehose_stream_encrypted_at_rest_test.py | 74 +++++++++++++++++-- 2 files changed, 69 insertions(+), 10 deletions(-) diff --git a/prowler/providers/aws/services/firehose/firehose_stream_encrypted_at_rest/firehose_stream_encrypted_at_rest.py b/prowler/providers/aws/services/firehose/firehose_stream_encrypted_at_rest/firehose_stream_encrypted_at_rest.py index fe63558366..0430b3d184 100644 --- a/prowler/providers/aws/services/firehose/firehose_stream_encrypted_at_rest/firehose_stream_encrypted_at_rest.py +++ b/prowler/providers/aws/services/firehose/firehose_stream_encrypted_at_rest/firehose_stream_encrypted_at_rest.py @@ -31,10 +31,7 @@ class firehose_stream_encrypted_at_rest(Check): f"Firehose Stream {stream.name} does have at rest encryption enabled." ) - if ( - stream.kms_encryption != EncryptionStatus.ENABLED - or not stream.kms_key_arn - ): + if stream.kms_encryption != EncryptionStatus.ENABLED: report.status = "FAIL" report.status_extended = f"Firehose Stream {stream.name} does not have at rest encryption enabled." diff --git a/tests/providers/aws/services/firehose/firehose_stream_encrypted_at_rest/firehose_stream_encrypted_at_rest_test.py b/tests/providers/aws/services/firehose/firehose_stream_encrypted_at_rest/firehose_stream_encrypted_at_rest_test.py index d2a1fa40a9..da00aa8900 100644 --- a/tests/providers/aws/services/firehose/firehose_stream_encrypted_at_rest/firehose_stream_encrypted_at_rest_test.py +++ b/tests/providers/aws/services/firehose/firehose_stream_encrypted_at_rest/firehose_stream_encrypted_at_rest_test.py @@ -17,12 +17,15 @@ class Test_firehose_stream_encrypted_at_rest: aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) - with mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=aws_provider, - ), mock.patch( - "prowler.providers.aws.services.firehose.firehose_stream_encrypted_at_rest.firehose_stream_encrypted_at_rest.firehose_client", - new=Firehose(aws_provider), + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.firehose.firehose_stream_encrypted_at_rest.firehose_stream_encrypted_at_rest.firehose_client", + new=Firehose(aws_provider), + ), ): # Test Check from prowler.providers.aws.services.firehose.firehose_stream_encrypted_at_rest.firehose_stream_encrypted_at_rest import ( @@ -94,6 +97,65 @@ class Test_firehose_stream_encrypted_at_rest: == f"Firehose Stream {stream_name} does have at rest encryption enabled." ) + @mock_aws + def test_stream_kms_encryption_enabled_aws_managed_key(self): + # Generate S3 client + s3_client = client("s3", region_name=AWS_REGION_EU_WEST_1) + s3_client.create_bucket( + Bucket="test-bucket", + CreateBucketConfiguration={"LocationConstraint": AWS_REGION_EU_WEST_1}, + ) + + # Generate Firehose client + firehose_client = client("firehose", region_name=AWS_REGION_EU_WEST_1) + delivery_stream = firehose_client.create_delivery_stream( + DeliveryStreamName="test-delivery-stream", + DeliveryStreamType="DirectPut", + S3DestinationConfiguration={ + "RoleARN": "arn:aws:iam::012345678901:role/firehose-role", + "BucketARN": "arn:aws:s3:::test-bucket", + "Prefix": "", + "BufferingHints": {"IntervalInSeconds": 300, "SizeInMBs": 5}, + "CompressionFormat": "UNCOMPRESSED", + }, + Tags=[{"Key": "key", "Value": "value"}], + ) + arn = delivery_stream["DeliveryStreamARN"] + stream_name = arn.split("/")[-1] + + firehose_client.start_delivery_stream_encryption( + DeliveryStreamName=stream_name, + DeliveryStreamEncryptionConfigurationInput={ + "KeyType": "AWS_OWNED_CMK", + }, + ) + + from prowler.providers.aws.services.firehose.firehose_service import Firehose + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.firehose.firehose_stream_encrypted_at_rest.firehose_stream_encrypted_at_rest.firehose_client", + new=Firehose(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.firehose.firehose_stream_encrypted_at_rest.firehose_stream_encrypted_at_rest import ( + firehose_stream_encrypted_at_rest, + ) + + check = firehose_stream_encrypted_at_rest() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Firehose Stream {stream_name} does have at rest encryption enabled." + ) + @mock_aws def test_stream_kms_encryption_not_enabled(self): # Generate Firehose client From 8d4b0914a8f46d74d6835ee8e4097396ca6320c5 Mon Sep 17 00:00:00 2001 From: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com> Date: Tue, 10 Dec 2024 20:34:22 +0100 Subject: [PATCH 39/67] fix(aws): get firewall manager managed rule groups (#6119) --- .../aws/services/wafv2/wafv2_service.py | 23 ++- .../wafv2_webacl_with_rules_test.py | 148 +++++++++++++++--- 2 files changed, 140 insertions(+), 31 deletions(-) diff --git a/prowler/providers/aws/services/wafv2/wafv2_service.py b/prowler/providers/aws/services/wafv2/wafv2_service.py index 85feed76f6..c867691b77 100644 --- a/prowler/providers/aws/services/wafv2/wafv2_service.py +++ b/prowler/providers/aws/services/wafv2/wafv2_service.py @@ -150,6 +150,22 @@ class WAFv2(AWSService): else: acl.rules.append(new_rule) + firewall_manager_managed_rg = get_web_acl.get("WebACL", {}).get( + "PreProcessFirewallManagerRuleGroups", [] + ) + get_web_acl.get("WebACL", {}).get( + "PostProcessFirewallManagerRuleGroups", [] + ) + + for rule in firewall_manager_managed_rg: + acl.rule_groups.append( + Rule( + name=rule.get("Name", ""), + cloudwatch_metrics_enabled=rule.get( + "VisibilityConfig", {} + ).get("CloudWatchMetricsEnabled", False), + ) + ) + except Exception as error: logger.error( f"{acl.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" @@ -193,13 +209,6 @@ class Rule(BaseModel): cloudwatch_metrics_enabled: bool = False -class FirewallManagerRuleGroup(BaseModel): - """Model representing a rule group for the Web ACL.""" - - name: str - cloudwatch_metrics_enabled: bool = False - - class WebAclv2(BaseModel): """Model representing a Web ACL for WAFv2.""" diff --git a/tests/providers/aws/services/wafv2/wafv2_webacl_with_rules/wafv2_webacl_with_rules_test.py b/tests/providers/aws/services/wafv2/wafv2_webacl_with_rules/wafv2_webacl_with_rules_test.py index 6d476d0f9b..19cde553a7 100644 --- a/tests/providers/aws/services/wafv2/wafv2_webacl_with_rules/wafv2_webacl_with_rules_test.py +++ b/tests/providers/aws/services/wafv2/wafv2_webacl_with_rules/wafv2_webacl_with_rules_test.py @@ -1,10 +1,61 @@ from unittest import mock +from unittest.mock import patch +import botocore from boto3 import client from moto import mock_aws from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider +# Original botocore _make_api_call function +orig = botocore.client.BaseClient._make_api_call + +FM_RG_NAME = "test-firewall-managed-rule-group" +FM_RG_ARN = "arn:aws:wafv2:us-east-1:123456789012:regional/webacl/test-firewall-managed-rule-group" + + +# Mocked botocore _make_api_call function +def mock_make_api_call(self, operation_name, kwarg): + if operation_name == "ListWebACLs": + return { + "WebACLs": [ + { + "Name": FM_RG_NAME, + "Id": FM_RG_NAME, + "ARN": FM_RG_ARN, + } + ] + } + elif operation_name == "GetWebACL": + return { + "WebACL": { + "PostProcessFirewallManagerRuleGroups": [ + { + "Name": FM_RG_NAME, + "VisibilityConfig": { + "SampledRequestsEnabled": True, + "CloudWatchMetricsEnabled": True, + "MetricName": "web-acl-test-metric", + }, + } + ] + } + } + elif operation_name == "ListResourcesForWebACL": + return { + "ResourceArns": [ + FM_RG_ARN, + ] + } + elif operation_name == "ListTagsForResource": + return { + "TagInfoForResource": { + "ResourceARN": FM_RG_ARN, + "TagList": [{"Key": "Name", "Value": FM_RG_NAME}], + } + } + return orig(self, operation_name, kwarg) + class Test_wafv2_webacl_with_rules: @mock_aws @@ -13,12 +64,15 @@ class Test_wafv2_webacl_with_rules: aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) - with mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=aws_provider, - ), mock.patch( - "prowler.providers.aws.services.wafv2.wafv2_webacl_with_rules.wafv2_webacl_with_rules.wafv2_client", - new=WAFv2(aws_provider), + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.wafv2.wafv2_webacl_with_rules.wafv2_webacl_with_rules.wafv2_client", + new=WAFv2(aws_provider), + ), ): from prowler.providers.aws.services.wafv2.wafv2_webacl_with_rules.wafv2_webacl_with_rules import ( wafv2_webacl_with_rules, @@ -69,12 +123,15 @@ class Test_wafv2_webacl_with_rules: aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) - with mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=aws_provider, - ), mock.patch( - "prowler.providers.aws.services.wafv2.wafv2_webacl_with_rules.wafv2_webacl_with_rules.wafv2_client", - new=WAFv2(aws_provider), + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.wafv2.wafv2_webacl_with_rules.wafv2_webacl_with_rules.wafv2_client", + new=WAFv2(aws_provider), + ), ): from prowler.providers.aws.services.wafv2.wafv2_webacl_with_rules.wafv2_webacl_with_rules import ( wafv2_webacl_with_rules, @@ -137,12 +194,15 @@ class Test_wafv2_webacl_with_rules: aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) - with mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=aws_provider, - ), mock.patch( - "prowler.providers.aws.services.wafv2.wafv2_webacl_with_rules.wafv2_webacl_with_rules.wafv2_client", - new=WAFv2(aws_provider), + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.wafv2.wafv2_webacl_with_rules.wafv2_webacl_with_rules.wafv2_client", + new=WAFv2(aws_provider), + ), ): from prowler.providers.aws.services.wafv2.wafv2_webacl_with_rules.wafv2_webacl_with_rules import ( wafv2_webacl_with_rules, @@ -161,6 +221,43 @@ class Test_wafv2_webacl_with_rules: assert result[0].region == AWS_REGION_US_EAST_1 assert result[0].resource_tags == [{"Key": "Name", "Value": waf_name}] + @patch( + "botocore.client.BaseClient._make_api_call", + new=mock_make_api_call, + ) + @mock_aws + def test_wafv2_web_acl_with_firewall_manager_managed_rule_group(self): + from prowler.providers.aws.services.wafv2.wafv2_service import WAFv2 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.wafv2.wafv2_webacl_with_rules.wafv2_webacl_with_rules.wafv2_client", + new=WAFv2(aws_provider), + ), + ): + from prowler.providers.aws.services.wafv2.wafv2_webacl_with_rules.wafv2_webacl_with_rules import ( + wafv2_webacl_with_rules, + ) + + check = wafv2_webacl_with_rules() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"AWS WAFv2 Web ACL {FM_RG_NAME} does have rules or rule groups attached." + ) + assert result[0].resource_id == FM_RG_NAME + assert result[0].resource_arn == FM_RG_ARN + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_tags == [{"Key": "Name", "Value": FM_RG_NAME}] + @mock_aws def test_wafv2_web_acl_without_rule_or_rule_group(self): wafv2_client = client("wafv2", region_name=AWS_REGION_US_EAST_1) @@ -184,12 +281,15 @@ class Test_wafv2_webacl_with_rules: aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) - with mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=aws_provider, - ), mock.patch( - "prowler.providers.aws.services.wafv2.wafv2_webacl_with_rules.wafv2_webacl_with_rules.wafv2_client", - new=WAFv2(aws_provider), + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.wafv2.wafv2_webacl_with_rules.wafv2_webacl_with_rules.wafv2_client", + new=WAFv2(aws_provider), + ), ): from prowler.providers.aws.services.wafv2.wafv2_webacl_with_rules.wafv2_webacl_with_rules import ( wafv2_webacl_with_rules, From 9c089756c3cc745f6cecb3aa1f091cf603834f10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Mart=C3=ADn?= Date: Tue, 10 Dec 2024 20:40:45 +0100 Subject: [PATCH 40/67] fix(compliance_tables): add correct values for findings (#6122) Co-authored-by: MrCloudSec --- prowler/lib/outputs/compliance/cis/cis.py | 7 ++++--- prowler/lib/outputs/compliance/ens/ens.py | 7 ++++--- prowler/lib/outputs/compliance/generic/generic_table.py | 7 ++++--- prowler/lib/outputs/compliance/kisa_ismsp/kisa_ismsp.py | 7 ++++--- .../lib/outputs/compliance/mitre_attack/mitre_attack.py | 7 ++++--- 5 files changed, 20 insertions(+), 15 deletions(-) diff --git a/prowler/lib/outputs/compliance/cis/cis.py b/prowler/lib/outputs/compliance/cis/cis.py index 7ab1a7d96c..30a68b2956 100644 --- a/prowler/lib/outputs/compliance/cis/cis.py +++ b/prowler/lib/outputs/compliance/cis/cis.py @@ -94,11 +94,12 @@ def get_cis_table( print( f"\nCompliance Status of {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL} Framework:" ) + total_findings_count = len(fail_count) + len(pass_count) + len(muted_count) overview_table = [ [ - f"{Fore.RED}{round(len(fail_count) / len(findings) * 100, 2)}% ({len(fail_count)}) FAIL{Style.RESET_ALL}", - f"{Fore.GREEN}{round(len(pass_count) / len(findings) * 100, 2)}% ({len(pass_count)}) PASS{Style.RESET_ALL}", - f"{orange_color}{round(len(muted_count) / len(findings) * 100, 2)}% ({len(muted_count)}) MUTED{Style.RESET_ALL}", + f"{Fore.RED}{round(len(fail_count) / total_findings_count * 100, 2)}% ({len(fail_count)}) FAIL{Style.RESET_ALL}", + f"{Fore.GREEN}{round(len(pass_count) / total_findings_count * 100, 2)}% ({len(pass_count)}) PASS{Style.RESET_ALL}", + f"{orange_color}{round(len(muted_count) / total_findings_count * 100, 2)}% ({len(muted_count)}) MUTED{Style.RESET_ALL}", ] ] print(tabulate(overview_table, tablefmt="rounded_grid")) diff --git a/prowler/lib/outputs/compliance/ens/ens.py b/prowler/lib/outputs/compliance/ens/ens.py index 64107f7ff7..a414a0206d 100644 --- a/prowler/lib/outputs/compliance/ens/ens.py +++ b/prowler/lib/outputs/compliance/ens/ens.py @@ -95,11 +95,12 @@ def get_ens_table( print( f"\nEstado de Cumplimiento de {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL}:" ) + total_findings_count = len(fail_count) + len(pass_count) + len(muted_count) overview_table = [ [ - f"{Fore.RED}{round(len(fail_count) / len(findings) * 100, 2)}% ({len(fail_count)}) NO CUMPLE{Style.RESET_ALL}", - f"{Fore.GREEN}{round(len(pass_count) / len(findings) * 100, 2)}% ({len(pass_count)}) CUMPLE{Style.RESET_ALL}", - f"{orange_color}{round(len(muted_count) / len(findings) * 100, 2)}% ({len(muted_count)}) MUTED{Style.RESET_ALL}", + f"{Fore.RED}{round(len(fail_count) / total_findings_count * 100, 2)}% ({len(fail_count)}) NO CUMPLE{Style.RESET_ALL}", + f"{Fore.GREEN}{round(len(pass_count) / total_findings_count * 100, 2)}% ({len(pass_count)}) CUMPLE{Style.RESET_ALL}", + f"{orange_color}{round(len(muted_count) / total_findings_count * 100, 2)}% ({len(muted_count)}) MUTED{Style.RESET_ALL}", ] ] print(tabulate(overview_table, tablefmt="rounded_grid")) diff --git a/prowler/lib/outputs/compliance/generic/generic_table.py b/prowler/lib/outputs/compliance/generic/generic_table.py index 570c6c9475..bede199eae 100644 --- a/prowler/lib/outputs/compliance/generic/generic_table.py +++ b/prowler/lib/outputs/compliance/generic/generic_table.py @@ -39,11 +39,12 @@ def get_generic_compliance_table( print( f"\nCompliance Status of {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL} Framework:" ) + total_findings_count = len(fail_count) + len(pass_count) + len(muted_count) overview_table = [ [ - f"{Fore.RED}{round(len(fail_count) / len(findings) * 100, 2)}% ({len(fail_count)}) FAIL{Style.RESET_ALL}", - f"{Fore.GREEN}{round(len(pass_count) / len(findings) * 100, 2)}% ({len(pass_count)}) PASS{Style.RESET_ALL}", - f"{orange_color}{round(len(muted_count) / len(findings) * 100, 2)}% ({len(muted_count)}) MUTED{Style.RESET_ALL}", + f"{Fore.RED}{round(len(fail_count) / total_findings_count * 100, 2)}% ({len(fail_count)}) FAIL{Style.RESET_ALL}", + f"{Fore.GREEN}{round(len(pass_count) / total_findings_count * 100, 2)}% ({len(pass_count)}) PASS{Style.RESET_ALL}", + f"{orange_color}{round(len(muted_count) / total_findings_count * 100, 2)}% ({len(muted_count)}) MUTED{Style.RESET_ALL}", ] ] print(tabulate(overview_table, tablefmt="rounded_grid")) diff --git a/prowler/lib/outputs/compliance/kisa_ismsp/kisa_ismsp.py b/prowler/lib/outputs/compliance/kisa_ismsp/kisa_ismsp.py index 8bfbff5b54..bf0a1b6e6f 100644 --- a/prowler/lib/outputs/compliance/kisa_ismsp/kisa_ismsp.py +++ b/prowler/lib/outputs/compliance/kisa_ismsp/kisa_ismsp.py @@ -61,11 +61,12 @@ def get_kisa_ismsp_table( print( f"\nCompliance Status of {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL} Framework:" ) + total_findings_count = len(fail_count) + len(pass_count) + len(muted_count) overview_table = [ [ - f"{Fore.RED}{round(len(fail_count) / len(findings) * 100, 2)}% ({len(fail_count)}) FAIL{Style.RESET_ALL}", - f"{Fore.GREEN}{round(len(pass_count) / len(findings) * 100, 2)}% ({len(pass_count)}) PASS{Style.RESET_ALL}", - f"{orange_color}{round(len(muted_count) / len(findings) * 100, 2)}% ({len(muted_count)}) MUTED{Style.RESET_ALL}", + f"{Fore.RED}{round(len(fail_count) / total_findings_count * 100, 2)}% ({len(fail_count)}) FAIL{Style.RESET_ALL}", + f"{Fore.GREEN}{round(len(pass_count) / total_findings_count * 100, 2)}% ({len(pass_count)}) PASS{Style.RESET_ALL}", + f"{orange_color}{round(len(muted_count) / total_findings_count * 100, 2)}% ({len(muted_count)}) MUTED{Style.RESET_ALL}", ] ] print(tabulate(overview_table, tablefmt="rounded_grid")) diff --git a/prowler/lib/outputs/compliance/mitre_attack/mitre_attack.py b/prowler/lib/outputs/compliance/mitre_attack/mitre_attack.py index 24c78e9469..bab3e4e31a 100644 --- a/prowler/lib/outputs/compliance/mitre_attack/mitre_attack.py +++ b/prowler/lib/outputs/compliance/mitre_attack/mitre_attack.py @@ -69,11 +69,12 @@ def get_mitre_attack_table( print( f"\nCompliance Status of {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL} Framework:" ) + total_findings_count = len(fail_count) + len(pass_count) + len(muted_count) overview_table = [ [ - f"{Fore.RED}{round(len(fail_count) / len(findings) * 100, 2)}% ({len(fail_count)}) FAIL{Style.RESET_ALL}", - f"{Fore.GREEN}{round(len(pass_count) / len(findings) * 100, 2)}% ({len(pass_count)}) PASS{Style.RESET_ALL}", - f"{orange_color}{round(len(muted_count) / len(findings) * 100, 2)}% ({len(muted_count)}) MUTED{Style.RESET_ALL}", + f"{Fore.RED}{round(len(fail_count) / total_findings_count * 100, 2)}% ({len(fail_count)}) FAIL{Style.RESET_ALL}", + f"{Fore.GREEN}{round(len(pass_count) / total_findings_count * 100, 2)}% ({len(pass_count)}) PASS{Style.RESET_ALL}", + f"{orange_color}{round(len(muted_count) / total_findings_count * 100, 2)}% ({len(muted_count)}) MUTED{Style.RESET_ALL}", ] ] print(tabulate(overview_table, tablefmt="rounded_grid")) From 989ccf4ae349d631b2f3b90bd82cb0512ee77e1a Mon Sep 17 00:00:00 2001 From: Sergio Garcia Date: Wed, 11 Dec 2024 04:13:49 -0400 Subject: [PATCH 41/67] fix(iam): set unique resource id for each user access key (#6128) --- .../iam_rotate_access_key_90_days.py | 4 +- .../iam_rotate_access_key_90_days_test.py | 76 +++++++++++-------- 2 files changed, 46 insertions(+), 34 deletions(-) diff --git a/prowler/providers/aws/services/iam/iam_rotate_access_key_90_days/iam_rotate_access_key_90_days.py b/prowler/providers/aws/services/iam/iam_rotate_access_key_90_days/iam_rotate_access_key_90_days.py index 3de61166c5..225b92381d 100644 --- a/prowler/providers/aws/services/iam/iam_rotate_access_key_90_days/iam_rotate_access_key_90_days.py +++ b/prowler/providers/aws/services/iam/iam_rotate_access_key_90_days/iam_rotate_access_key_90_days.py @@ -49,7 +49,7 @@ class iam_rotate_access_key_90_days(Check): old_access_keys = True report = Check_Report_AWS(self.metadata()) report.region = iam_client.region - report.resource_id = user["user"] + report.resource_id = f"{user['user']}-access-key-1" report.resource_arn = user["arn"] report.resource_tags = user_tags report.status = "FAIL" @@ -66,7 +66,7 @@ class iam_rotate_access_key_90_days(Check): old_access_keys = True report = Check_Report_AWS(self.metadata()) report.region = iam_client.region - report.resource_id = user["user"] + report.resource_id = f"{user['user']}-access-key-2" report.resource_arn = user["arn"] report.resource_tags = user_tags report.status = "FAIL" diff --git a/tests/providers/aws/services/iam/iam_rotate_access_key_90_days/iam_rotate_access_key_90_days_test.py b/tests/providers/aws/services/iam/iam_rotate_access_key_90_days/iam_rotate_access_key_90_days_test.py index 57c697062f..8205cd583c 100644 --- a/tests/providers/aws/services/iam/iam_rotate_access_key_90_days/iam_rotate_access_key_90_days_test.py +++ b/tests/providers/aws/services/iam/iam_rotate_access_key_90_days/iam_rotate_access_key_90_days_test.py @@ -21,13 +21,16 @@ class Test_iam_rotate_access_key_90_days_test: aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) - with mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=aws_provider, - ), mock.patch( - "prowler.providers.aws.services.iam.iam_rotate_access_key_90_days.iam_rotate_access_key_90_days.iam_client", - new=IAM(aws_provider), - ) as service_client: + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.iam.iam_rotate_access_key_90_days.iam_rotate_access_key_90_days.iam_client", + new=IAM(aws_provider), + ) as service_client, + ): from prowler.providers.aws.services.iam.iam_rotate_access_key_90_days.iam_rotate_access_key_90_days import ( iam_rotate_access_key_90_days, ) @@ -62,13 +65,16 @@ class Test_iam_rotate_access_key_90_days_test: aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) - with mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=aws_provider, - ), mock.patch( - "prowler.providers.aws.services.iam.iam_rotate_access_key_90_days.iam_rotate_access_key_90_days.iam_client", - new=IAM(aws_provider), - ) as service_client: + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.iam.iam_rotate_access_key_90_days.iam_rotate_access_key_90_days.iam_client", + new=IAM(aws_provider), + ) as service_client, + ): from prowler.providers.aws.services.iam.iam_rotate_access_key_90_days.iam_rotate_access_key_90_days import ( iam_rotate_access_key_90_days, ) @@ -86,7 +92,7 @@ class Test_iam_rotate_access_key_90_days_test: result[0].status_extended == f"User {user} has not rotated access key 1 in over 90 days (100 days)." ) - assert result[0].resource_id == user + assert result[0].resource_id == f"{user}-access-key-1" assert result[0].resource_arn == arn assert result[0].region == AWS_REGION_US_EAST_1 assert result[0].resource_tags == [{"Key": "test-tag", "Value": "test"}] @@ -106,13 +112,16 @@ class Test_iam_rotate_access_key_90_days_test: aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) - with mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=aws_provider, - ), mock.patch( - "prowler.providers.aws.services.iam.iam_rotate_access_key_90_days.iam_rotate_access_key_90_days.iam_client", - new=IAM(aws_provider), - ) as service_client: + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.iam.iam_rotate_access_key_90_days.iam_rotate_access_key_90_days.iam_client", + new=IAM(aws_provider), + ) as service_client, + ): from prowler.providers.aws.services.iam.iam_rotate_access_key_90_days.iam_rotate_access_key_90_days import ( iam_rotate_access_key_90_days, ) @@ -130,7 +139,7 @@ class Test_iam_rotate_access_key_90_days_test: result[0].status_extended == f"User {user} has not rotated access key 2 in over 90 days (100 days)." ) - assert result[0].resource_id == user + assert result[0].resource_id == f"{user}-access-key-2" assert result[0].resource_arn == arn assert result[0].region == AWS_REGION_US_EAST_1 assert result[0].resource_tags == [{"Key": "test-tag", "Value": "test"}] @@ -150,13 +159,16 @@ class Test_iam_rotate_access_key_90_days_test: aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) - with mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=aws_provider, - ), mock.patch( - "prowler.providers.aws.services.iam.iam_rotate_access_key_90_days.iam_rotate_access_key_90_days.iam_client", - new=IAM(aws_provider), - ) as service_client: + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.iam.iam_rotate_access_key_90_days.iam_rotate_access_key_90_days.iam_client", + new=IAM(aws_provider), + ) as service_client, + ): from prowler.providers.aws.services.iam.iam_rotate_access_key_90_days.iam_rotate_access_key_90_days import ( iam_rotate_access_key_90_days, ) @@ -179,7 +191,7 @@ class Test_iam_rotate_access_key_90_days_test: result[0].status_extended == f"User {user} has not rotated access key 1 in over 90 days (100 days)." ) - assert result[0].resource_id == user + assert result[0].resource_id == f"{user}-access-key-1" assert result[0].resource_arn == arn assert result[0].region == AWS_REGION_US_EAST_1 assert result[0].resource_tags == [{"Key": "test-tag", "Value": "test"}] @@ -188,7 +200,7 @@ class Test_iam_rotate_access_key_90_days_test: result[1].status_extended == f"User {user} has not rotated access key 2 in over 90 days (100 days)." ) - assert result[1].resource_id == user + assert result[1].resource_id == f"{user}-access-key-2" assert result[1].resource_arn == arn assert result[1].region == AWS_REGION_US_EAST_1 assert result[1].resource_tags == [{"Key": "test-tag", "Value": "test"}] From 4d1c8eae8fbbdedea19ff473faafc9e5c1f1d6df Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Wed, 11 Dec 2024 10:05:30 +0100 Subject: [PATCH 42/67] feat(users): user detail can be edited now properly (#6135) --- ui/actions/users/users.ts | 32 ++++++++++++++++--------- ui/components/users/forms/edit-form.tsx | 24 ++++++++++++------- ui/types/formSchemas.ts | 19 ++------------- 3 files changed, 38 insertions(+), 37 deletions(-) diff --git a/ui/actions/users/users.ts b/ui/actions/users/users.ts index cf3397a940..5f8cfa4b25 100644 --- a/ui/actions/users/users.ts +++ b/ui/actions/users/users.ts @@ -51,14 +51,28 @@ export const updateUser = async (formData: FormData) => { const session = await auth(); const keyServer = process.env.API_BASE_URL; - const userId = formData.get("userId"); - const userName = formData.get("name"); - const userPassword = formData.get("password"); - const userEmail = formData.get("email"); - const userCompanyName = formData.get("company_name"); + const userId = formData.get("userId") as string; // Ensure userId is a string + const userName = formData.get("name") as string | null; + const userPassword = formData.get("password") as string | null; + const userEmail = formData.get("email") as string | null; + const userCompanyName = formData.get("company_name") as string | null; const url = new URL(`${keyServer}/users/${userId}`); + // Prepare attributes to send based on changes + const attributes: Record = {}; + + // Add only changed fields + if (userName !== null) attributes.name = userName; + if (userEmail !== null) attributes.email = userEmail; + if (userCompanyName !== null) attributes.company_name = userCompanyName; + if (userPassword !== null) attributes.password = userPassword; + + // If no fields have changed, don't send the request + if (Object.keys(attributes).length === 0) { + return { error: "No changes detected" }; + } + try { const response = await fetch(url.toString(), { method: "PATCH", @@ -71,15 +85,11 @@ export const updateUser = async (formData: FormData) => { data: { type: "users", id: userId, - attributes: { - name: userName, - password: userPassword, - email: userEmail, - company_name: userCompanyName, - }, + attributes: attributes, }, }), }); + const data = await response.json(); revalidatePath("/users"); return parseStringify(data); diff --git a/ui/components/users/forms/edit-form.tsx b/ui/components/users/forms/edit-form.tsx index ece7080b5a..8f6788ac8f 100644 --- a/ui/components/users/forms/edit-form.tsx +++ b/ui/components/users/forms/edit-form.tsx @@ -25,11 +25,7 @@ export const EditForm = ({ userCompanyName?: string; setIsOpen: Dispatch>; }) => { - const formSchema = editUserFormSchema( - userName ?? "", - userEmail ?? "", - userCompanyName ?? "", - ); + const formSchema = editUserFormSchema(); const form = useForm>({ resolver: zodResolver(formSchema), @@ -48,16 +44,26 @@ export const EditForm = ({ const onSubmitClient = async (values: z.infer) => { const formData = new FormData(); - Object.entries(values).forEach( - ([key, value]) => value !== undefined && formData.append(key, value), - ); + // Check if the value is not undefined before appending to FormData + if (values.name !== undefined) { + formData.append("name", values.name); + } + if (values.email !== undefined) { + formData.append("email", values.email); + } + if (values.company_name !== undefined) { + formData.append("company_name", values.company_name); + } + + // Always include userId + formData.append("userId", userId); const data = await updateUser(formData); if (data?.errors && data.errors.length > 0) { const error = data.errors[0]; const errorMessage = `${error.detail}`; - // show error + // Show error toast({ variant: "destructive", title: "Oops! Something went wrong", diff --git a/ui/types/formSchemas.ts b/ui/types/formSchemas.ts index cc0221add7..0b503fe26b 100644 --- a/ui/types/formSchemas.ts +++ b/ui/types/formSchemas.ts @@ -160,36 +160,21 @@ export const editInviteFormSchema = z.object({ expires_at: z.string().optional(), }); -export const editUserFormSchema = ( - currentName: string, - currentEmail: string, - currentCompanyName: string, -) => +export const editUserFormSchema = () => z.object({ name: z .string() .min(3, { message: "The name must have at least 3 characters." }) .max(150, { message: "The name cannot exceed 150 characters." }) - .refine((val) => val !== currentName, { - message: "The new name must be different from the current one.", - }) .optional(), email: z .string() .email({ message: "Please enter a valid email address." }) - .refine((val) => val !== currentEmail, { - message: "The new email must be different from the current one.", - }) .optional(), password: z .string() .min(1, { message: "The password cannot be empty." }) .optional(), - company_name: z - .string() - .refine((val) => val !== currentCompanyName, { - message: "The new company name must be different from the current one.", - }) - .optional(), + company_name: z.string().optional(), userId: z.string(), }); From d7b0bc02ba6ad8a528d7630ea56cf906568e8056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20De=20la=20Torre=20Vico?= Date: Wed, 11 Dec 2024 13:14:29 +0100 Subject: [PATCH 43/67] feat(app): add support for TLS 1.3 to Web Apps check (#6004) --- .../app_minimum_tls_version_12.py | 9 ++-- .../app_minimum_tls_version_12_test.py | 42 +++++++++++++++++++ 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/prowler/providers/azure/services/app/app_minimum_tls_version_12/app_minimum_tls_version_12.py b/prowler/providers/azure/services/app/app_minimum_tls_version_12/app_minimum_tls_version_12.py index 32020e2ec2..519335a016 100644 --- a/prowler/providers/azure/services/app/app_minimum_tls_version_12/app_minimum_tls_version_12.py +++ b/prowler/providers/azure/services/app/app_minimum_tls_version_12/app_minimum_tls_version_12.py @@ -19,12 +19,11 @@ class app_minimum_tls_version_12(Check): report.location = app.location report.status_extended = f"Minimum TLS version is not set to 1.2 for app '{app_name}' in subscription '{subscription_name}'." - if ( - app.configurations - and getattr(app.configurations, "min_tls_version", "") == "1.2" - ): + if app.configurations and getattr( + app.configurations, "min_tls_version", "" + ) in ["1.2", "1.3"]: report.status = "PASS" - report.status_extended = f"Minimum TLS version is set to 1.2 for app '{app_name}' in subscription '{subscription_name}'." + report.status_extended = f"Minimum TLS version is set to {app.configurations.min_tls_version} for app '{app_name}' in subscription '{subscription_name}'." findings.append(report) diff --git a/tests/providers/azure/services/app/app_minimum_tls_version_12/app_minimum_tls_version_12_test.py b/tests/providers/azure/services/app/app_minimum_tls_version_12/app_minimum_tls_version_12_test.py index da0271cbc0..12a149015e 100644 --- a/tests/providers/azure/services/app/app_minimum_tls_version_12/app_minimum_tls_version_12_test.py +++ b/tests/providers/azure/services/app/app_minimum_tls_version_12/app_minimum_tls_version_12_test.py @@ -171,3 +171,45 @@ class Test_app_minimum_tls_version_12: assert result[0].resource_name == "app_id-1" assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].location == "West Europe" + + def test_app_min_tls_version_13(self): + resource_id = f"/subscriptions/{uuid4()}" + app_client = mock.MagicMock + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), mock.patch( + "prowler.providers.azure.services.app.app_minimum_tls_version_12.app_minimum_tls_version_12.app_client", + new=app_client, + ): + from prowler.providers.azure.services.app.app_minimum_tls_version_12.app_minimum_tls_version_12 import ( + app_minimum_tls_version_12, + ) + from prowler.providers.azure.services.app.app_service import WebApp + + app_client.apps = { + AZURE_SUBSCRIPTION_ID: { + "app_id-1": WebApp( + resource_id=resource_id, + auth_enabled=False, + configurations=mock.MagicMock(min_tls_version="1.3"), + client_cert_mode="Ignore", + https_only=False, + identity=None, + location="West Europe", + ) + } + } + check = app_minimum_tls_version_12() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Minimum TLS version is set to 1.3 for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_ID}'." + ) + assert result[0].resource_id == resource_id + assert result[0].resource_name == "app_id-1" + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].location == "West Europe" From ec3fddf5b14ef835561d774f84ec75a5889f37a7 Mon Sep 17 00:00:00 2001 From: Prowler Bot Date: Wed, 11 Dec 2024 14:25:17 +0100 Subject: [PATCH 44/67] chore(regions_update): Changes in regions for AWS services (#6136) Co-authored-by: MrCloudSec <38561120+MrCloudSec@users.noreply.github.com> --- prowler/providers/aws/aws_regions_by_service.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/prowler/providers/aws/aws_regions_by_service.json b/prowler/providers/aws/aws_regions_by_service.json index d58ae09536..a0b21fc92c 100644 --- a/prowler/providers/aws/aws_regions_by_service.json +++ b/prowler/providers/aws/aws_regions_by_service.json @@ -10895,6 +10895,7 @@ "ca-west-1", "eu-central-1", "eu-central-2", + "eu-north-1", "eu-south-1", "eu-south-2", "eu-west-1", @@ -10902,6 +10903,7 @@ "eu-west-3", "il-central-1", "me-central-1", + "me-south-1", "sa-east-1", "us-east-1", "us-east-2", @@ -11095,6 +11097,7 @@ "ap-southeast-1", "ap-southeast-2", "ap-southeast-3", + "ap-southeast-4", "ca-central-1", "eu-central-1", "eu-central-2", From 805ed810318d9f9f0838cfc3dc99bc141c30ecf7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Dec 2024 09:26:07 -0400 Subject: [PATCH 45/67] chore(deps): bump boto3 from 1.35.76 to 1.35.77 (#6131) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 10 +++++----- pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index 0bb5c90d09..329e1eb712 100644 --- a/poetry.lock +++ b/poetry.lock @@ -775,17 +775,17 @@ files = [ [[package]] name = "boto3" -version = "1.35.76" +version = "1.35.77" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" files = [ - {file = "boto3-1.35.76-py3-none-any.whl", hash = "sha256:69458399f41f57a50770c8974796d96978bcca44915c260319696bb43e47dffd"}, - {file = "boto3-1.35.76.tar.gz", hash = "sha256:31ddcdb6f15dace2b68f6a0f11bdb58dd3ae79b8a3ccb174ff811ef0bbf938e0"}, + {file = "boto3-1.35.77-py3-none-any.whl", hash = "sha256:a09871805f8e462349a1c33c23eb413668df0bf68424e61d53518e1a7d883b2f"}, + {file = "boto3-1.35.77.tar.gz", hash = "sha256:cc819cdbccbc2d0dc185f1dcfe74cf3809489c4cae63c2e5d6a557aa0c5ab928"}, ] [package.dependencies] -botocore = ">=1.35.76,<1.36.0" +botocore = ">=1.35.77,<1.36.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.10.0,<0.11.0" @@ -5194,4 +5194,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "f1f58e6a345fb6e33aa3d47d2a65070491b0e1b58c9a92db786ce8ea8ebbb30f" +content-hash = "f94001d4038c14dc1e1a7b2e711ed24180ee0dfc3f3a55d1cc55cf8c945ffb09" diff --git a/pyproject.toml b/pyproject.toml index d62a0d941c..b6fe1ae289 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ azure-mgmt-storage = "21.2.1" azure-mgmt-subscription = "3.1.1" azure-mgmt-web = "7.3.1" azure-storage-blob = "12.24.0" -boto3 = "1.35.76" +boto3 = "1.35.77" botocore = "1.35.77" colorama = "0.4.6" cryptography = "43.0.1" From 0c0887afef61cbe7edee593f78809156daaa8b85 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Dec 2024 09:29:14 -0400 Subject: [PATCH 46/67] chore(deps): bump trufflesecurity/trufflehog from 3.85.0 to 3.86.0 (#6130) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/find-secrets.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/find-secrets.yml b/.github/workflows/find-secrets.yml index c89d9d0fae..c7ca25f34c 100644 --- a/.github/workflows/find-secrets.yml +++ b/.github/workflows/find-secrets.yml @@ -11,7 +11,7 @@ jobs: with: fetch-depth: 0 - name: TruffleHog OSS - uses: trufflesecurity/trufflehog@v3.85.0 + uses: trufflesecurity/trufflehog@v3.86.0 with: path: ./ base: ${{ github.event.repository.default_branch }} From b486007f951aecd3c632912080ac1f924e1a5a2b Mon Sep 17 00:00:00 2001 From: Sergio Garcia Date: Wed, 11 Dec 2024 10:19:06 -0400 Subject: [PATCH 47/67] fix(README): show latest release (#6145) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 145ba4048b..98d5cd2476 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@

Repo size Issues - Version + Version Version Contributors License From 0758e976285b25684b75764072d5e8ef9b37de79 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Dec 2024 10:19:37 -0400 Subject: [PATCH 48/67] chore(deps): bump botocore from 1.35.77 to 1.35.78 (#6132) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index 329e1eb712..a949279fe0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -794,13 +794,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.35.77" +version = "1.35.78" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" files = [ - {file = "botocore-1.35.77-py3-none-any.whl", hash = "sha256:3faa27d65841499762228902d7e215fa99a4c2fdc76c9113e1c3f339bdf685b8"}, - {file = "botocore-1.35.77.tar.gz", hash = "sha256:17b778016644e9342ca3ff2f430c1d1db0c6126e9b41a57cff52ac58e7a455e0"}, + {file = "botocore-1.35.78-py3-none-any.whl", hash = "sha256:41c37bd7c0326f25122f33ec84fb80fc0a14d7fcc9961431b0e57568e88c9cb5"}, + {file = "botocore-1.35.78.tar.gz", hash = "sha256:6905036c25449ae8dba5e950e4b908e4b8a6fe6b516bf61e007ecb62fa21f323"}, ] [package.dependencies] @@ -5194,4 +5194,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "f94001d4038c14dc1e1a7b2e711ed24180ee0dfc3f3a55d1cc55cf8c945ffb09" +content-hash = "ea4e52926a0b10ccc9109beaa54ac3fb3c02090dd8bd0dc127f303d28f7f3894" diff --git a/pyproject.toml b/pyproject.toml index b6fe1ae289..69279330ca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ azure-mgmt-subscription = "3.1.1" azure-mgmt-web = "7.3.1" azure-storage-blob = "12.24.0" boto3 = "1.35.77" -botocore = "1.35.77" +botocore = "1.35.78" colorama = "0.4.6" cryptography = "43.0.1" dash = "2.18.2" From 9b434d48566a6272cefa6ea7151dccfffb8001af Mon Sep 17 00:00:00 2001 From: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com> Date: Wed, 11 Dec 2024 15:42:11 +0100 Subject: [PATCH 49/67] feat(ecr): add new fixer `ecr_repositories_not_publicly_accessible_fixer` (#5923) --- ...ositories_not_publicly_accessible_fixer.py | 38 +++++ ...ries_not_publicly_accessible_fixer_test.py | 135 ++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 prowler/providers/aws/services/ecr/ecr_repositories_not_publicly_accessible/ecr_repositories_not_publicly_accessible_fixer.py create mode 100644 tests/providers/aws/services/ecr/ecr_repositories_not_publicly_accessible/ecr_repositories_not_publicly_accessible_fixer_test.py diff --git a/prowler/providers/aws/services/ecr/ecr_repositories_not_publicly_accessible/ecr_repositories_not_publicly_accessible_fixer.py b/prowler/providers/aws/services/ecr/ecr_repositories_not_publicly_accessible/ecr_repositories_not_publicly_accessible_fixer.py new file mode 100644 index 0000000000..6c43385e73 --- /dev/null +++ b/prowler/providers/aws/services/ecr/ecr_repositories_not_publicly_accessible/ecr_repositories_not_publicly_accessible_fixer.py @@ -0,0 +1,38 @@ +from prowler.lib.logger import logger +from prowler.providers.aws.services.ecr.ecr_client import ecr_client + + +def fixer(resource_id: str, region: str) -> bool: + """ + Modify the ECR repository's policy to remove public access. + Specifically, this fixer delete the policy that had public access. + Requires the ecr:DeleteRepositoryPolicy permission. + Permissions: + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "ecr:DeleteRepositoryPolicy", + "Resource": "*" + } + ] + } + Args: + resource_id (str): The ECR repository name. + region (str): AWS region where the ECR repository exists. + Returns: + bool: True if the operation is successful (policy updated), False otherwise. + """ + try: + regional_client = ecr_client.regional_clients[region] + + regional_client.delete_repository_policy(repositoryName=resource_id) + + except Exception as error: + logger.error( + f"{region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return False + else: + return True diff --git a/tests/providers/aws/services/ecr/ecr_repositories_not_publicly_accessible/ecr_repositories_not_publicly_accessible_fixer_test.py b/tests/providers/aws/services/ecr/ecr_repositories_not_publicly_accessible/ecr_repositories_not_publicly_accessible_fixer_test.py new file mode 100644 index 0000000000..d2b4953cdc --- /dev/null +++ b/tests/providers/aws/services/ecr/ecr_repositories_not_publicly_accessible/ecr_repositories_not_publicly_accessible_fixer_test.py @@ -0,0 +1,135 @@ +from json import dumps +from unittest import mock + +from boto3 import client +from moto import mock_aws + +from tests.providers.aws.utils import AWS_REGION_EU_WEST_1, set_mocked_aws_provider + + +class Test_ecr_repositories_not_publicly_accessible_fixer: + @mock_aws + def test_ecr_repository_public(self): + ecr_client = client("ecr", region_name=AWS_REGION_EU_WEST_1) + + repository_name = "test-repo" + ecr_client.create_repository(repositoryName=repository_name) + + ecr_client.set_repository_policy( + repositoryName=repository_name, + policyText=dumps( + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"AWS": "*"}, + "Action": "ecr:*", + "Resource": f"arn:aws:ecr:{AWS_REGION_EU_WEST_1}:123456789012:repository/{repository_name}", + } + ], + } + ), + ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + from prowler.providers.aws.services.ecr.ecr_service import ECR + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.ecr.ecr_repositories_not_publicly_accessible.ecr_repositories_not_publicly_accessible_fixer.ecr_client", + new=ECR(aws_provider), + ): + # Test Fixer + from prowler.providers.aws.services.ecr.ecr_repositories_not_publicly_accessible.ecr_repositories_not_publicly_accessible_fixer import ( + fixer, + ) + + assert fixer(repository_name, AWS_REGION_EU_WEST_1) + + @mock_aws + def test_ecr_repository_not_public(self): + ecr_client = client("ecr", region_name=AWS_REGION_EU_WEST_1) + + repository_name = "test-repo" + ecr_client.create_repository(repositoryName=repository_name) + + ecr_client.set_repository_policy( + repositoryName=repository_name, + policyText=dumps( + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"AWS": "123456789012"}, + "Action": "ecr:*", + "Resource": f"arn:aws:ecr:{AWS_REGION_EU_WEST_1}:123456789012:repository/{repository_name}", + } + ], + } + ), + ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + from prowler.providers.aws.services.ecr.ecr_service import ECR + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.ecr.ecr_repositories_not_publicly_accessible.ecr_repositories_not_publicly_accessible_fixer.ecr_client", + new=ECR(aws_provider), + ): + # Test Fixer + from prowler.providers.aws.services.ecr.ecr_repositories_not_publicly_accessible.ecr_repositories_not_publicly_accessible_fixer import ( + fixer, + ) + + assert fixer(repository_name, AWS_REGION_EU_WEST_1) + + @mock_aws + def test_ecr_repository_public_error(self): + ecr_client = client("ecr", region_name=AWS_REGION_EU_WEST_1) + + repository_name = "test-repo" + ecr_client.create_repository(repositoryName=repository_name) + + ecr_client.set_repository_policy( + repositoryName=repository_name, + policyText=dumps( + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"AWS": "*"}, + "Action": "ecr:*", + "Resource": f"arn:aws:ecr:{AWS_REGION_EU_WEST_1}:123456789012:repository/{repository_name}", + } + ], + } + ), + ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + from prowler.providers.aws.services.ecr.ecr_service import ECR + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.ecr.ecr_repositories_not_publicly_accessible.ecr_repositories_not_publicly_accessible_fixer.ecr_client", + new=ECR(aws_provider), + ): + # Test Fixer + from prowler.providers.aws.services.ecr.ecr_repositories_not_publicly_accessible.ecr_repositories_not_publicly_accessible_fixer import ( + fixer, + ) + + assert not fixer("repository_name_non_existing", AWS_REGION_EU_WEST_1) From 07178ac69a927900fdac06e7258625ecc3c231fe Mon Sep 17 00:00:00 2001 From: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:10:12 +0100 Subject: [PATCH 50/67] feat(glacier): add new fixer `glacier_vaults_policy_public_access_fixer` (#5950) --- ...acier_vaults_policy_public_access_fixer.py | 38 +++++++++ ..._vaults_policy_public_access_fixer_test.py | 81 +++++++++++++++++++ ...lacier_vaults_policy_public_access_test.py | 18 +++++ 3 files changed, 137 insertions(+) create mode 100644 prowler/providers/aws/services/glacier/glacier_vaults_policy_public_access/glacier_vaults_policy_public_access_fixer.py create mode 100644 tests/providers/aws/services/glacier/glacier_vaults_policy_public_access/glacier_vaults_policy_public_access_fixer_test.py diff --git a/prowler/providers/aws/services/glacier/glacier_vaults_policy_public_access/glacier_vaults_policy_public_access_fixer.py b/prowler/providers/aws/services/glacier/glacier_vaults_policy_public_access/glacier_vaults_policy_public_access_fixer.py new file mode 100644 index 0000000000..c9362e07bf --- /dev/null +++ b/prowler/providers/aws/services/glacier/glacier_vaults_policy_public_access/glacier_vaults_policy_public_access_fixer.py @@ -0,0 +1,38 @@ +from prowler.lib.logger import logger +from prowler.providers.aws.services.glacier.glacier_client import glacier_client + + +def fixer(resource_id: str, region: str) -> bool: + """ + Modify the Glacier vault's policy to remove public access. + Specifically, this fixer delete the vault policy that has public access. + Requires the glacier:DeleteVaultAccessPolicy permission. + Permissions: + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "glacier:DeleteVaultAccessPolicy", + "Resource": "*" + } + ] + } + Args: + resource_id (str): The Glacier vault name. + region (str): AWS region where the Glacier vault exists. + Returns: + bool: True if the operation is successful (policy updated), False otherwise. + """ + try: + regional_client = glacier_client.regional_clients[region] + + regional_client.delete_vault_access_policy(vaultName=resource_id) + + except Exception as error: + logger.error( + f"{region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return False + else: + return True diff --git a/tests/providers/aws/services/glacier/glacier_vaults_policy_public_access/glacier_vaults_policy_public_access_fixer_test.py b/tests/providers/aws/services/glacier/glacier_vaults_policy_public_access/glacier_vaults_policy_public_access_fixer_test.py new file mode 100644 index 0000000000..87c808d722 --- /dev/null +++ b/tests/providers/aws/services/glacier/glacier_vaults_policy_public_access/glacier_vaults_policy_public_access_fixer_test.py @@ -0,0 +1,81 @@ +from unittest import mock + +import botocore +from moto import mock_aws + +from tests.providers.aws.utils import AWS_REGION_EU_WEST_1, set_mocked_aws_provider + +mock_make_api_call = botocore.client.BaseClient._make_api_call + + +def mock_make_api_call_public_vault(self, operation_name, kwarg): + if operation_name == "DeleteVaultAccessPolicy": + return { + "ResponseMetadata": { + "HTTPStatusCode": 204, + "RequestId": "test-request-id", + } + } + return mock_make_api_call(self, operation_name, kwarg) + + +def mock_make_api_call_public_vault_error(self, operation_name, kwarg): + if operation_name == "DeleteVaultAccessPolicy": + raise botocore.exceptions.ClientError( + { + "Error": { + "Code": "VaultNotFound", + "Message": "VaultNotFound", + } + }, + operation_name, + ) + return mock_make_api_call(self, operation_name, kwarg) + + +class Test_glacier_vaults_policy_public_access_fixer: + @mock_aws + def test_glacier_vault_public(self): + with mock.patch( + "botocore.client.BaseClient._make_api_call", + new=mock_make_api_call_public_vault, + ): + from prowler.providers.aws.services.glacier.glacier_service import Glacier + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.glacier.glacier_vaults_policy_public_access.glacier_vaults_policy_public_access_fixer.glacier_client", + new=Glacier(aws_provider), + ): + from prowler.providers.aws.services.glacier.glacier_vaults_policy_public_access.glacier_vaults_policy_public_access_fixer import ( + fixer, + ) + + assert fixer(resource_id="test-vault", region=AWS_REGION_EU_WEST_1) + + @mock_aws + def test_glacier_vault_public_error(self): + with mock.patch( + "botocore.client.BaseClient._make_api_call", + new=mock_make_api_call_public_vault_error, + ): + from prowler.providers.aws.services.glacier.glacier_service import Glacier + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.glacier.glacier_vaults_policy_public_access.glacier_vaults_policy_public_access_fixer.glacier_client", + new=Glacier(aws_provider), + ): + from prowler.providers.aws.services.glacier.glacier_vaults_policy_public_access.glacier_vaults_policy_public_access_fixer import ( + fixer, + ) + + assert not fixer(resource_id="test-vault", region=AWS_REGION_EU_WEST_1) diff --git a/tests/providers/aws/services/glacier/glacier_vaults_policy_public_access/glacier_vaults_policy_public_access_test.py b/tests/providers/aws/services/glacier/glacier_vaults_policy_public_access/glacier_vaults_policy_public_access_test.py index 2d10ed7d97..d267e1bc66 100644 --- a/tests/providers/aws/services/glacier/glacier_vaults_policy_public_access/glacier_vaults_policy_public_access_test.py +++ b/tests/providers/aws/services/glacier/glacier_vaults_policy_public_access/glacier_vaults_policy_public_access_test.py @@ -11,6 +11,9 @@ class Test_glacier_vaults_policy_public_access: with mock.patch( "prowler.providers.aws.services.glacier.glacier_service.Glacier", new=glacier_client, + ), mock.patch( + "prowler.providers.aws.services.glacier.glacier_client.glacier_client", + new=glacier_client, ): # Test Check from prowler.providers.aws.services.glacier.glacier_vaults_policy_public_access.glacier_vaults_policy_public_access import ( @@ -37,6 +40,9 @@ class Test_glacier_vaults_policy_public_access: with mock.patch( "prowler.providers.aws.services.glacier.glacier_service.Glacier", new=glacier_client, + ), mock.patch( + "prowler.providers.aws.services.glacier.glacier_client.glacier_client", + new=glacier_client, ): # Test Check from prowler.providers.aws.services.glacier.glacier_vaults_policy_public_access.glacier_vaults_policy_public_access import ( @@ -89,6 +95,9 @@ class Test_glacier_vaults_policy_public_access: with mock.patch( "prowler.providers.aws.services.glacier.glacier_service.Glacier", new=glacier_client, + ), mock.patch( + "prowler.providers.aws.services.glacier.glacier_client.glacier_client", + new=glacier_client, ): # Test Check from prowler.providers.aws.services.glacier.glacier_vaults_policy_public_access.glacier_vaults_policy_public_access import ( @@ -141,6 +150,9 @@ class Test_glacier_vaults_policy_public_access: with mock.patch( "prowler.providers.aws.services.glacier.glacier_service.Glacier", new=glacier_client, + ), mock.patch( + "prowler.providers.aws.services.glacier.glacier_client.glacier_client", + new=glacier_client, ): # Test Check from prowler.providers.aws.services.glacier.glacier_vaults_policy_public_access.glacier_vaults_policy_public_access import ( @@ -193,6 +205,9 @@ class Test_glacier_vaults_policy_public_access: with mock.patch( "prowler.providers.aws.services.glacier.glacier_service.Glacier", new=glacier_client, + ), mock.patch( + "prowler.providers.aws.services.glacier.glacier_client.glacier_client", + new=glacier_client, ): # Test Check from prowler.providers.aws.services.glacier.glacier_vaults_policy_public_access.glacier_vaults_policy_public_access import ( @@ -249,6 +264,9 @@ class Test_glacier_vaults_policy_public_access: with mock.patch( "prowler.providers.aws.services.glacier.glacier_service.Glacier", new=glacier_client, + ), mock.patch( + "prowler.providers.aws.services.glacier.glacier_client.glacier_client", + new=glacier_client, ): # Test Check from prowler.providers.aws.services.glacier.glacier_vaults_policy_public_access.glacier_vaults_policy_public_access import ( From a915c04e9e778539c61ece067189fa45840c6cd2 Mon Sep 17 00:00:00 2001 From: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:18:30 +0100 Subject: [PATCH 51/67] fix(autoscaling): `autoscaling_group_launch_configuration_requires_imdsv2` fails if Launch Template is used (#6111) Co-authored-by: Sergio Garcia --- ...group_launch_configuration_no_public_ip.py | 25 ++++++++++--------- ...up_launch_configuration_requires_imdsv2.py | 19 ++++++-------- ..._launch_configuration_no_public_ip_test.py | 14 +---------- ...unch_configuration_requires_imdsv2_test.py | 14 +---------- 4 files changed, 23 insertions(+), 49 deletions(-) diff --git a/prowler/providers/aws/services/autoscaling/autoscaling_group_launch_configuration_no_public_ip/autoscaling_group_launch_configuration_no_public_ip.py b/prowler/providers/aws/services/autoscaling/autoscaling_group_launch_configuration_no_public_ip/autoscaling_group_launch_configuration_no_public_ip.py index 3d2eefa3b7..178a6748a6 100644 --- a/prowler/providers/aws/services/autoscaling/autoscaling_group_launch_configuration_no_public_ip/autoscaling_group_launch_configuration_no_public_ip.py +++ b/prowler/providers/aws/services/autoscaling/autoscaling_group_launch_configuration_no_public_ip/autoscaling_group_launch_configuration_no_public_ip.py @@ -8,19 +8,20 @@ class autoscaling_group_launch_configuration_no_public_ip(Check): def execute(self): findings = [] for group in autoscaling_client.groups: - report = Check_Report_AWS(self.metadata()) - report.region = group.region - report.resource_id = group.name - report.resource_arn = group.arn - report.resource_tags = group.tags - report.status = "PASS" - report.status_extended = f"Autoscaling group {group.name} does not have an associated launch configuration assigning a public IP address." - for lc in autoscaling_client.launch_configurations.values(): - if lc.name == group.launch_configuration_name and lc.public_ip: - report.status = "FAIL" - report.status_extended = f"Autoscaling group {group.name} has an associated launch configuration assigning a public IP address." + if lc.name == group.launch_configuration_name: + report = Check_Report_AWS(self.metadata()) + report.region = group.region + report.resource_id = group.name + report.resource_arn = group.arn + report.resource_tags = group.tags + report.status = "PASS" + report.status_extended = f"Autoscaling group {group.name} does not have an associated launch configuration assigning a public IP address." - findings.append(report) + if lc.public_ip: + report.status = "FAIL" + report.status_extended = f"Autoscaling group {group.name} has an associated launch configuration assigning a public IP address." + + findings.append(report) return findings diff --git a/prowler/providers/aws/services/autoscaling/autoscaling_group_launch_configuration_requires_imdsv2/autoscaling_group_launch_configuration_requires_imdsv2.py b/prowler/providers/aws/services/autoscaling/autoscaling_group_launch_configuration_requires_imdsv2/autoscaling_group_launch_configuration_requires_imdsv2.py index b27e4c9a52..5d79ba42d9 100644 --- a/prowler/providers/aws/services/autoscaling/autoscaling_group_launch_configuration_requires_imdsv2/autoscaling_group_launch_configuration_requires_imdsv2.py +++ b/prowler/providers/aws/services/autoscaling/autoscaling_group_launch_configuration_requires_imdsv2/autoscaling_group_launch_configuration_requires_imdsv2.py @@ -8,20 +8,17 @@ class autoscaling_group_launch_configuration_requires_imdsv2(Check): def execute(self): findings = [] for group in autoscaling_client.groups: - report = Check_Report_AWS(self.metadata()) - report.region = group.region - report.resource_id = group.name - report.resource_arn = group.arn - report.resource_tags = group.tags - report.status = "FAIL" - report.status_extended = ( - f"Autoscaling group {group.name} has IMDSv2 disabled or not required." - ) - for ( launch_configuration ) in autoscaling_client.launch_configurations.values(): if launch_configuration.name == group.launch_configuration_name: + report = Check_Report_AWS(self.metadata()) + report.region = group.region + report.resource_id = group.name + report.resource_arn = group.arn + report.resource_tags = group.tags + report.status = "FAIL" + report.status_extended = f"Autoscaling group {group.name} has IMDSv2 disabled or not required." if ( launch_configuration.http_endpoint == "enabled" and launch_configuration.http_tokens == "required" @@ -32,6 +29,6 @@ class autoscaling_group_launch_configuration_requires_imdsv2(Check): report.status = "PASS" report.status_extended = f"Autoscaling group {group.name} has metadata service disabled." - findings.append(report) + findings.append(report) return findings diff --git a/tests/providers/aws/services/autoscaling/autoscaling_group_launch_configuration_no_public_ip/autoscaling_group_launch_configuration_no_public_ip_test.py b/tests/providers/aws/services/autoscaling/autoscaling_group_launch_configuration_no_public_ip/autoscaling_group_launch_configuration_no_public_ip_test.py index 7362e3ad7c..a911e0169d 100644 --- a/tests/providers/aws/services/autoscaling/autoscaling_group_launch_configuration_no_public_ip/autoscaling_group_launch_configuration_no_public_ip_test.py +++ b/tests/providers/aws/services/autoscaling/autoscaling_group_launch_configuration_no_public_ip/autoscaling_group_launch_configuration_no_public_ip_test.py @@ -171,10 +171,6 @@ class Test_autoscaling_group_launch_configuration_no_public_ip: AvailabilityZones=["us-east-1a", "us-east-1b"], ) - autoscaling_group_arn = autoscaling_client.describe_auto_scaling_groups( - AutoScalingGroupNames=[autoscaling_group_name] - )["AutoScalingGroups"][0]["AutoScalingGroupARN"] - from prowler.providers.aws.services.autoscaling.autoscaling_service import ( AutoScaling, ) @@ -196,12 +192,4 @@ class Test_autoscaling_group_launch_configuration_no_public_ip: check = autoscaling_group_launch_configuration_no_public_ip() result = check.execute() - assert len(result) == 1 - assert result[0].status == "PASS" - assert ( - result[0].status_extended - == f"Autoscaling group {autoscaling_group_name} does not have an associated launch configuration assigning a public IP address." - ) - assert result[0].resource_id == autoscaling_group_name - assert result[0].resource_tags == [] - assert result[0].resource_arn == autoscaling_group_arn + assert len(result) == 0 diff --git a/tests/providers/aws/services/autoscaling/autoscaling_group_launch_configuration_requires_imdsv2/autoscaling_group_launch_configuration_requires_imdsv2_test.py b/tests/providers/aws/services/autoscaling/autoscaling_group_launch_configuration_requires_imdsv2/autoscaling_group_launch_configuration_requires_imdsv2_test.py index b1f769626f..a9239a3ed1 100644 --- a/tests/providers/aws/services/autoscaling/autoscaling_group_launch_configuration_requires_imdsv2/autoscaling_group_launch_configuration_requires_imdsv2_test.py +++ b/tests/providers/aws/services/autoscaling/autoscaling_group_launch_configuration_requires_imdsv2/autoscaling_group_launch_configuration_requires_imdsv2_test.py @@ -119,10 +119,6 @@ class Test_autoscaling_group_launch_configuration_requires_imdsv2: AvailabilityZones=["us-east-1a", "us-east-1b"], ) - autoscaling_group_arn = autoscaling_client.describe_auto_scaling_groups( - AutoScalingGroupNames=[autoscaling_group_name] - )["AutoScalingGroups"][0]["AutoScalingGroupARN"] - from prowler.providers.aws.services.autoscaling.autoscaling_service import ( AutoScaling, ) @@ -144,15 +140,7 @@ class Test_autoscaling_group_launch_configuration_requires_imdsv2: check = autoscaling_group_launch_configuration_requires_imdsv2() result = check.execute() - assert len(result) == 1 - assert result[0].status == "FAIL" - assert ( - result[0].status_extended - == f"Autoscaling group {autoscaling_group_name} has IMDSv2 disabled or not required." - ) - assert result[0].resource_id == autoscaling_group_name - assert result[0].resource_tags == [] - assert result[0].resource_arn == autoscaling_group_arn + assert len(result) == 0 @mock_aws def test_groups_with_imdsv2_disabled(self): From fdb1956b0b504c68cb3545259505d32cea010654 Mon Sep 17 00:00:00 2001 From: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:29:48 +0100 Subject: [PATCH 52/67] feat(opensearch): add new fixer `opensearch_service_domains_not_publicly_accessible_fixer` (#5926) --- ...e_domains_not_publicly_accessible_fixer.py | 43 ++++++ ...ains_not_publicly_accessible_fixer_test.py | 135 ++++++++++++++++++ 2 files changed, 178 insertions(+) create mode 100644 prowler/providers/aws/services/opensearch/opensearch_service_domains_not_publicly_accessible/opensearch_service_domains_not_publicly_accessible_fixer.py create mode 100644 tests/providers/aws/services/opensearch/opensearch_service_domains_not_publicly_accessible/opensearch_service_domains_not_publicly_accessible_fixer_test.py diff --git a/prowler/providers/aws/services/opensearch/opensearch_service_domains_not_publicly_accessible/opensearch_service_domains_not_publicly_accessible_fixer.py b/prowler/providers/aws/services/opensearch/opensearch_service_domains_not_publicly_accessible/opensearch_service_domains_not_publicly_accessible_fixer.py new file mode 100644 index 0000000000..a43a94fc6e --- /dev/null +++ b/prowler/providers/aws/services/opensearch/opensearch_service_domains_not_publicly_accessible/opensearch_service_domains_not_publicly_accessible_fixer.py @@ -0,0 +1,43 @@ +from prowler.lib.logger import logger +from prowler.providers.aws.services.opensearch.opensearch_client import ( + opensearch_client, +) + + +def fixer(resource_id: str, region: str) -> bool: + """ + Modify the OpenSearch domain's resource-based policy to remove public access. + Specifically, this fixer update the domain config and add an empty policy to remove the old one. + Requires the es:UpdateDomainConfig permission. + Permissions: + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "es:UpdateDomainConfig", + "Resource": "*" + } + ] + } + Args: + resource_id (str): The OpenSearch domain name. + region (str): AWS region where the OpenSearch domain exists. + Returns: + bool: True if the operation is successful (policy updated), False otherwise. + """ + try: + regional_client = opensearch_client.regional_clients[region] + + regional_client.update_domain_config( + DomainName=resource_id, + AccessPolicies="", + ) + + except Exception as error: + logger.error( + f"{region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return False + else: + return True diff --git a/tests/providers/aws/services/opensearch/opensearch_service_domains_not_publicly_accessible/opensearch_service_domains_not_publicly_accessible_fixer_test.py b/tests/providers/aws/services/opensearch/opensearch_service_domains_not_publicly_accessible/opensearch_service_domains_not_publicly_accessible_fixer_test.py new file mode 100644 index 0000000000..01bb37f03d --- /dev/null +++ b/tests/providers/aws/services/opensearch/opensearch_service_domains_not_publicly_accessible/opensearch_service_domains_not_publicly_accessible_fixer_test.py @@ -0,0 +1,135 @@ +from json import dumps +from unittest import mock + +from boto3 import client +from moto import mock_aws + +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_US_WEST_2, + set_mocked_aws_provider, +) + +domain_name = "test-domain" + +policy_data_restricted = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"AWS": [f"{AWS_ACCOUNT_NUMBER}"]}, + "Action": ["es:*"], + "Resource": f"arn:aws:es:us-west-2:{AWS_ACCOUNT_NUMBER}:domain/{domain_name}/*", + } + ], +} + +policy_data_not_restricted = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"AWS": ["*"]}, + "Action": ["es:*"], + "Resource": f"arn:aws:es:us-west-2:{AWS_ACCOUNT_NUMBER}:domain/{domain_name}/*", + } + ], +} + +policy_data_not_restricted_principal = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": "*", + "Action": ["es:*"], + "Resource": f"arn:aws:es:us-west-2:{AWS_ACCOUNT_NUMBER}:domain/{domain_name}/*", + } + ], +} + + +class Test_opensearch_service_domains_not_publicly_accessible_fixer: + @mock_aws + def test_policy_data_restricted_error(self): + opensearch_client = client("opensearch", region_name=AWS_REGION_US_WEST_2) + opensearch_client.create_domain(DomainName=domain_name)["DomainStatus"]["ARN"] + opensearch_client.update_domain_config( + DomainName=domain_name, + AccessPolicies=str(policy_data_restricted), + ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_WEST_2]) + + from prowler.providers.aws.services.opensearch.opensearch_service import ( + OpenSearchService, + ) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.opensearch.opensearch_service_domains_not_publicly_accessible.opensearch_service_domains_not_publicly_accessible_fixer.opensearch_client", + new=OpenSearchService(aws_provider), + ): + from prowler.providers.aws.services.opensearch.opensearch_service_domains_not_publicly_accessible.opensearch_service_domains_not_publicly_accessible_fixer import ( + fixer, + ) + + assert not fixer("domain_name_non_existing", AWS_REGION_US_WEST_2) + + @mock_aws + def test_policy_data_not_restricted_with_principal_AWS(self): + opensearch_client = client("opensearch", region_name=AWS_REGION_US_WEST_2) + opensearch_client.create_domain(DomainName=domain_name)["DomainStatus"]["ARN"] + opensearch_client.update_domain_config( + DomainName=domain_name, + AccessPolicies=dumps(policy_data_not_restricted), + ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_WEST_2]) + + from prowler.providers.aws.services.opensearch.opensearch_service import ( + OpenSearchService, + ) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.opensearch.opensearch_service_domains_not_publicly_accessible.opensearch_service_domains_not_publicly_accessible_fixer.opensearch_client", + new=OpenSearchService(aws_provider), + ): + from prowler.providers.aws.services.opensearch.opensearch_service_domains_not_publicly_accessible.opensearch_service_domains_not_publicly_accessible_fixer import ( + fixer, + ) + + assert fixer(domain_name, AWS_REGION_US_WEST_2) + + @mock_aws + def test_policy_data_not_restricted_with_principal_no_AWS(self): + opensearch_client = client("opensearch", region_name=AWS_REGION_US_WEST_2) + opensearch_client.create_domain(DomainName=domain_name)["DomainStatus"]["ARN"] + opensearch_client.update_domain_config( + DomainName=domain_name, + AccessPolicies=dumps(policy_data_not_restricted_principal), + ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_WEST_2]) + + from prowler.providers.aws.services.opensearch.opensearch_service import ( + OpenSearchService, + ) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.opensearch.opensearch_service_domains_not_publicly_accessible.opensearch_service_domains_not_publicly_accessible_fixer.opensearch_client", + new=OpenSearchService(aws_provider), + ): + from prowler.providers.aws.services.opensearch.opensearch_service_domains_not_publicly_accessible.opensearch_service_domains_not_publicly_accessible_fixer import ( + fixer, + ) + + assert fixer(domain_name, AWS_REGION_US_WEST_2) From eded97d73541188cefd254b7dac19d601c566b7d Mon Sep 17 00:00:00 2001 From: johannes-engler-mw <132657752+johannes-engler-mw@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:37:53 +0100 Subject: [PATCH 53/67] feat(azure): check for minimal TLS version for Azure SQL server (#5745) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Rubén De la Torre Vico --- docs/tutorials/configuration_file.md | 9 + prowler/config/config.yaml | 8 + .../__init__.py | 0 ...ommended_minimal_tls_version.metadata.json | 30 +++ ...lserver_recommended_minimal_tls_version.py | 27 +++ tests/config/config_test.py | 1 + tests/config/fixtures/config.yaml | 8 + tests/providers/azure/azure_provider_test.py | 1 + ...er_recommended_minimal_tls_version_test.py | 215 ++++++++++++++++++ 9 files changed, 299 insertions(+) create mode 100644 prowler/providers/azure/services/sqlserver/sqlserver_recommended_minimal_tls_version/__init__.py create mode 100644 prowler/providers/azure/services/sqlserver/sqlserver_recommended_minimal_tls_version/sqlserver_recommended_minimal_tls_version.metadata.json create mode 100644 prowler/providers/azure/services/sqlserver/sqlserver_recommended_minimal_tls_version/sqlserver_recommended_minimal_tls_version.py create mode 100644 tests/providers/azure/services/sqlserver/sqlserver_recommended_minimal_tls_version/sqlserver_recommended_minimal_tls_version_test.py diff --git a/docs/tutorials/configuration_file.md b/docs/tutorials/configuration_file.md index 866c7f8dff..b638728b27 100644 --- a/docs/tutorials/configuration_file.md +++ b/docs/tutorials/configuration_file.md @@ -75,6 +75,7 @@ The following list includes all the Azure checks with configurable variables tha | `app_ensure_php_version_is_latest` | `php_latest_version` | String | | `app_ensure_python_version_is_latest` | `python_latest_version` | String | | `app_ensure_java_version_is_latest` | `java_latest_version` | String | +| `sqlserver_recommended_minimal_tls_version` | `recommended_minimal_tls_versions` | List of Strings | ## GCP @@ -447,6 +448,14 @@ azure: # azure.app_ensure_java_version_is_latest java_latest_version: "17" + # Azure SQL Server + # azure.sqlserver_minimal_tls_version + recommended_minimal_tls_versions: + [ + "1.2", + "1.3" + ] + # GCP Configuration gcp: # GCP Compute Configuration diff --git a/prowler/config/config.yaml b/prowler/config/config.yaml index 8b6947b458..f9641eba56 100644 --- a/prowler/config/config.yaml +++ b/prowler/config/config.yaml @@ -388,6 +388,14 @@ azure: # azure.app_ensure_java_version_is_latest java_latest_version: "17" + # Azure SQL Server + # azure.sqlserver_minimal_tls_version + recommended_minimal_tls_versions: + [ + "1.2", + "1.3", + ] + # GCP Configuration gcp: # GCP Compute Configuration diff --git a/prowler/providers/azure/services/sqlserver/sqlserver_recommended_minimal_tls_version/__init__.py b/prowler/providers/azure/services/sqlserver/sqlserver_recommended_minimal_tls_version/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/azure/services/sqlserver/sqlserver_recommended_minimal_tls_version/sqlserver_recommended_minimal_tls_version.metadata.json b/prowler/providers/azure/services/sqlserver/sqlserver_recommended_minimal_tls_version/sqlserver_recommended_minimal_tls_version.metadata.json new file mode 100644 index 0000000000..42e35c2c4f --- /dev/null +++ b/prowler/providers/azure/services/sqlserver/sqlserver_recommended_minimal_tls_version/sqlserver_recommended_minimal_tls_version.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "azure", + "CheckID": "sqlserver_recommended_minimal_tls_version", + "CheckTitle": "Ensure SQL server has a recommended minimal TLS version required.", + "CheckType": [], + "ServiceName": "sqlserver", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "SQLServer", + "Description": "Ensure that SQL Server instances are configured with the recommended minimal TLS version to maintain secure connections.", + "Risk": "Using outdated or weak TLS versions can expose SQL Server instances to vulnerabilities, increasing the risk of data breaches and unauthorized access.", + "RelatedUrl": "https://learn.microsoft.com/en-us/azure/azure-sql/database/connectivity-settings?view=azuresql&tabs=azure-portal#configure-minimum-tls-version", + "Remediation": { + "Code": { + "CLI": "az sql server update -n sql-server-name -g sql-server-group --set minimalTlsVersion=", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "1. Go to Azure SQL Server 2. Navigate to 'Security' -> 'Networking' 3. Select 'Connectivity' 4. Update the TLS version in the field 'Minimum TLS version' to a recommended minimal version (e.g., TLS 1.2).", + "Url": "https://learn.microsoft.com/en-us/azure/azure-sql/database/connectivity-settings?view=azuresql&tabs=azure-portal#configure-minimum-tls-version" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Verify support for the TLS version from the application side before changing the minimal version." +} diff --git a/prowler/providers/azure/services/sqlserver/sqlserver_recommended_minimal_tls_version/sqlserver_recommended_minimal_tls_version.py b/prowler/providers/azure/services/sqlserver/sqlserver_recommended_minimal_tls_version/sqlserver_recommended_minimal_tls_version.py new file mode 100644 index 0000000000..d0f97b8861 --- /dev/null +++ b/prowler/providers/azure/services/sqlserver/sqlserver_recommended_minimal_tls_version/sqlserver_recommended_minimal_tls_version.py @@ -0,0 +1,27 @@ +from typing import List + +from prowler.lib.check.models import Check, Check_Report_Azure +from prowler.providers.azure.services.sqlserver.sqlserver_client import sqlserver_client + + +class sqlserver_recommended_minimal_tls_version(Check): + def execute(self) -> List[Check_Report_Azure]: + findings = [] + recommended_minimal_tls_versions = sqlserver_client.audit_config.get( + "recommended_minimal_tls_versions", ["1.2", "1.3"] + ) + for subscription, sql_servers in sqlserver_client.sql_servers.items(): + for sql_server in sql_servers: + report = Check_Report_Azure(self.metadata()) + report.subscription = subscription + report.resource_name = sql_server.name + report.resource_id = sql_server.id + report.status = "FAIL" + report.location = sql_server.location + report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription} is using TLS version {sql_server.minimal_tls_version} as minimal accepted which is not recommended. Please use one of the recommended versions: {', '.join(recommended_minimal_tls_versions)}." + if sql_server.minimal_tls_version in recommended_minimal_tls_versions: + report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription} is using version {sql_server.minimal_tls_version} as minimal accepted which is recommended." + report.status = "PASS" + findings.append(report) + + return findings diff --git a/tests/config/config_test.py b/tests/config/config_test.py index f79ef50eff..14e0077a88 100644 --- a/tests/config/config_test.py +++ b/tests/config/config_test.py @@ -318,6 +318,7 @@ config_azure = { "php_latest_version": "8.2", "python_latest_version": "3.12", "java_latest_version": "17", + "recommended_minimal_tls_versions": ["1.2", "1.3"], } config_gcp = {"shodan_api_key": None} diff --git a/tests/config/fixtures/config.yaml b/tests/config/fixtures/config.yaml index 54b1c011b3..0994a496dc 100644 --- a/tests/config/fixtures/config.yaml +++ b/tests/config/fixtures/config.yaml @@ -379,6 +379,14 @@ azure: # azure.app_ensure_java_version_is_latest java_latest_version: "17" + # Azure SQL Server + # azure.sqlserver_minimal_tls_version + recommended_minimal_tls_versions: + [ + "1.2", + "1.3" + ] + # GCP Configuration gcp: # GCP Compute Configuration diff --git a/tests/providers/azure/azure_provider_test.py b/tests/providers/azure/azure_provider_test.py index ea798ad057..853903fcc2 100644 --- a/tests/providers/azure/azure_provider_test.py +++ b/tests/providers/azure/azure_provider_test.py @@ -81,6 +81,7 @@ class TestAzureProvider: "php_latest_version": "8.2", "python_latest_version": "3.12", "java_latest_version": "17", + "recommended_minimal_tls_versions": ["1.2", "1.3"], } def test_azure_provider_not_auth_methods(self): diff --git a/tests/providers/azure/services/sqlserver/sqlserver_recommended_minimal_tls_version/sqlserver_recommended_minimal_tls_version_test.py b/tests/providers/azure/services/sqlserver/sqlserver_recommended_minimal_tls_version/sqlserver_recommended_minimal_tls_version_test.py new file mode 100644 index 0000000000..2b80067468 --- /dev/null +++ b/tests/providers/azure/services/sqlserver/sqlserver_recommended_minimal_tls_version/sqlserver_recommended_minimal_tls_version_test.py @@ -0,0 +1,215 @@ +from unittest import mock +from uuid import uuid4 + +from azure.mgmt.sql.models import TransparentDataEncryption + +from prowler.providers.azure.services.sqlserver.sqlserver_service import ( + Database, + Server, +) +from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_ID, + set_mocked_azure_provider, +) + + +class Test_sqlserver_recommended_minimal_tls_version: + def test_no_sql_servers(self): + sqlserver_client = mock.MagicMock + sqlserver_client.sql_servers = {} + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), mock.patch( + "prowler.providers.azure.services.sqlserver.sqlserver_recommended_minimal_tls_version.sqlserver_recommended_minimal_tls_version.sqlserver_client", + new=sqlserver_client, + ): + from prowler.providers.azure.services.sqlserver.sqlserver_recommended_minimal_tls_version.sqlserver_recommended_minimal_tls_version import ( + sqlserver_recommended_minimal_tls_version, + ) + + sqlserver_client.audit_config = { + "recommended_minimal_tls_versions": ["1.2", "1.3"] + } + + check = sqlserver_recommended_minimal_tls_version() + result = check.execute() + assert len(result) == 0 + + def test_sql_servers_deprecated_minimal_tls_version(self): + sqlserver_client = mock.MagicMock + sql_server_name = "SQL Server Name" + sql_server_id = str(uuid4()) + database_name = "Database Name" + database_id = str(uuid4()) + database = Database( + id=database_id, + name=database_name, + type="type", + location="location", + managed_by="managed_by", + tde_encryption=TransparentDataEncryption(status="Disabled"), + ) + sqlserver_client.sql_servers = { + AZURE_SUBSCRIPTION_ID: [ + Server( + id=sql_server_id, + name=sql_server_name, + public_network_access="", + minimal_tls_version="1.0", + administrators=None, + auditing_policies=None, + firewall_rules=None, + databases=[database], + encryption_protector=None, + location="location", + ) + ] + } + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), mock.patch( + "prowler.providers.azure.services.sqlserver.sqlserver_recommended_minimal_tls_version.sqlserver_recommended_minimal_tls_version.sqlserver_client", + new=sqlserver_client, + ): + from prowler.providers.azure.services.sqlserver.sqlserver_recommended_minimal_tls_version.sqlserver_recommended_minimal_tls_version import ( + sqlserver_recommended_minimal_tls_version, + ) + + sqlserver_client.audit_config = { + "recommended_minimal_tls_versions": ["1.2", "1.3"] + } + + check = sqlserver_recommended_minimal_tls_version() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} is using TLS version 1.0 as minimal accepted which is not recommended. Please use one of the recommended versions: {', '.join(sqlserver_client.audit_config['recommended_minimal_tls_versions'])}." + ) + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == sql_server_name + assert result[0].resource_id == sql_server_id + assert result[0].location == "location" + + def test_sql_servers_no_minimal_tls_version(self): + sqlserver_client = mock.MagicMock + sql_server_name = "SQL Server Name" + sql_server_id = str(uuid4()) + database_name = "Database Name" + database_id = str(uuid4()) + database = Database( + id=database_id, + name=database_name, + type="type", + location="location", + managed_by="managed_by", + tde_encryption=TransparentDataEncryption(status="Disabled"), + ) + sqlserver_client.sql_servers = { + AZURE_SUBSCRIPTION_ID: [ + Server( + id=sql_server_id, + name=sql_server_name, + public_network_access="", + minimal_tls_version="", + administrators=None, + auditing_policies=None, + firewall_rules=None, + databases=[database], + encryption_protector=None, + location="location", + ) + ] + } + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), mock.patch( + "prowler.providers.azure.services.sqlserver.sqlserver_recommended_minimal_tls_version.sqlserver_recommended_minimal_tls_version.sqlserver_client", + new=sqlserver_client, + ): + from prowler.providers.azure.services.sqlserver.sqlserver_recommended_minimal_tls_version.sqlserver_recommended_minimal_tls_version import ( + sqlserver_recommended_minimal_tls_version, + ) + + sqlserver_client.audit_config = { + "recommended_minimal_tls_versions": ["1.2", "1.3"] + } + + check = sqlserver_recommended_minimal_tls_version() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} is using TLS version as minimal accepted which is not recommended. Please use one of the recommended versions: {', '.join(sqlserver_client.audit_config['recommended_minimal_tls_versions'])}." + ) + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == sql_server_name + assert result[0].resource_id == sql_server_id + assert result[0].location == "location" + + def test_sql_servers_minimal_tls_version(self): + sqlserver_client = mock.MagicMock + sql_server_name = "SQL Server Name" + sql_server_id = str(uuid4()) + database_name = "Database Name" + database_id = str(uuid4()) + database = Database( + id=database_id, + name=database_name, + type="type", + location="location", + managed_by="managed_by", + tde_encryption=TransparentDataEncryption(status="Enabled"), + ) + sqlserver_client.sql_servers = { + AZURE_SUBSCRIPTION_ID: [ + Server( + id=sql_server_id, + name=sql_server_name, + public_network_access="", + minimal_tls_version="1.2", + administrators=None, + auditing_policies=None, + firewall_rules=None, + databases=[database], + encryption_protector=None, + location="location", + ) + ] + } + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), mock.patch( + "prowler.providers.azure.services.sqlserver.sqlserver_recommended_minimal_tls_version.sqlserver_recommended_minimal_tls_version.sqlserver_client", + new=sqlserver_client, + ): + from prowler.providers.azure.services.sqlserver.sqlserver_recommended_minimal_tls_version.sqlserver_recommended_minimal_tls_version import ( + sqlserver_recommended_minimal_tls_version, + ) + + sqlserver_client.audit_config = { + "recommended_minimal_tls_versions": ["1.2", "1.3"] + } + + check = sqlserver_recommended_minimal_tls_version() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} is using version 1.2 as minimal accepted which is recommended." + ) + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == sql_server_name + assert result[0].resource_id == sql_server_id + assert result[0].location == "location" From bd9673c9deaebbd52b6303dd991d9bbe6821d090 Mon Sep 17 00:00:00 2001 From: Mads Brouer Lundholm Date: Wed, 11 Dec 2024 09:01:45 -0800 Subject: [PATCH 54/67] fix(aurora): Add default ports to the check of using non default ports (#5821) Co-authored-by: Mads Rantala Lundholm Co-authored-by: Sergio Garcia --- .../rds_cluster_non_default_port.py | 4 +- .../rds_instance_non_default_port.py | 4 +- .../rds_cluster_non_default_port_test.py | 220 ++++++++++- .../rds_instance_non_default_port_test.py | 346 +++++++++++++++++- 4 files changed, 539 insertions(+), 35 deletions(-) diff --git a/prowler/providers/aws/services/rds/rds_cluster_non_default_port/rds_cluster_non_default_port.py b/prowler/providers/aws/services/rds/rds_cluster_non_default_port/rds_cluster_non_default_port.py index dfbb39fb77..f1e149db1d 100644 --- a/prowler/providers/aws/services/rds/rds_cluster_non_default_port/rds_cluster_non_default_port.py +++ b/prowler/providers/aws/services/rds/rds_cluster_non_default_port/rds_cluster_non_default_port.py @@ -6,8 +6,8 @@ class rds_cluster_non_default_port(Check): def execute(self): findings = [] default_ports = { - 3306: ["mysql", "mariadb"], - 5432: ["postgres"], + 3306: ["mysql", "mariadb", "aurora-mysql"], + 5432: ["postgres", "aurora-postgresql"], 1521: ["oracle"], 1433: ["sqlserver"], 50000: ["db2"], diff --git a/prowler/providers/aws/services/rds/rds_instance_non_default_port/rds_instance_non_default_port.py b/prowler/providers/aws/services/rds/rds_instance_non_default_port/rds_instance_non_default_port.py index 9e2dda34ed..17a5bc65ef 100644 --- a/prowler/providers/aws/services/rds/rds_instance_non_default_port/rds_instance_non_default_port.py +++ b/prowler/providers/aws/services/rds/rds_instance_non_default_port/rds_instance_non_default_port.py @@ -6,8 +6,8 @@ class rds_instance_non_default_port(Check): def execute(self): findings = [] default_ports = { - 3306: ["mysql", "mariadb"], - 5432: ["postgres"], + 3306: ["mysql", "mariadb", "aurora-mysql"], + 5432: ["postgres", "aurora-postgresql"], 1521: ["oracle"], 1433: ["sqlserver"], 50000: ["db2"], diff --git a/tests/providers/aws/services/rds/rds_cluster_non_default_port/rds_cluster_non_default_port_test.py b/tests/providers/aws/services/rds/rds_cluster_non_default_port/rds_cluster_non_default_port_test.py index 5315b16965..50c4ae1b5c 100644 --- a/tests/providers/aws/services/rds/rds_cluster_non_default_port/rds_cluster_non_default_port_test.py +++ b/tests/providers/aws/services/rds/rds_cluster_non_default_port/rds_cluster_non_default_port_test.py @@ -35,7 +35,7 @@ class Test_rds_cluster_non_default_port: assert len(result) == 0 @mock_aws - def test_rds_cluster_using_default_port(self): + def test_rds_cluster_aurora_postgres_using_default_port(self): conn = client("rds", region_name=AWS_REGION_US_EAST_1) conn.create_db_cluster( DBClusterIdentifier="db-cluster-1", @@ -82,10 +82,10 @@ class Test_rds_cluster_non_default_port: assert result[0].resource_tags == [{"Key": "test", "Value": "test"}] @mock_aws - def test_rds_cluster_using_non_default_port(self): + def test_rds_cluster_aurora_postgres_using_non_default_port(self): conn = client("rds", region_name=AWS_REGION_US_EAST_1) conn.create_db_cluster( - DBClusterIdentifier="db-cluster-1", + DBClusterIdentifier="db-cluster-2", Engine="aurora-postgresql", StorageEncrypted=True, DeletionProtection=True, @@ -118,13 +118,205 @@ class Test_rds_cluster_non_default_port: assert result[0].status == "PASS" assert ( result[0].status_extended - == "RDS Cluster db-cluster-1 is not using the default port 5433 for aurora-postgresql." + == "RDS Cluster db-cluster-2 is not using the default port 5433 for aurora-postgresql." ) - assert result[0].resource_id == "db-cluster-1" + assert result[0].resource_id == "db-cluster-2" assert result[0].region == AWS_REGION_US_EAST_1 assert ( result[0].resource_arn - == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:cluster:db-cluster-1" + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:cluster:db-cluster-2" + ) + assert result[0].resource_tags == [ + {"Key": "env", "Value": "production"} + ] + + @mock_aws + def test_rds_cluster_postgres_using_default_port(self): + conn = client("rds", region_name=AWS_REGION_US_EAST_1) + conn.create_db_cluster( + DBClusterIdentifier="db-cluster-3", + Engine="postgres", + StorageEncrypted=True, + DeletionProtection=True, + MasterUsername="cluster", + MasterUserPassword="password", + Port=5432, + Tags=[{"Key": "test", "Value": "test"}], + ) + + from prowler.providers.aws.services.rds.rds_service import RDS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_cluster_non_default_port.rds_cluster_non_default_port.rds_client", + new=RDS(aws_provider), + ): + from prowler.providers.aws.services.rds.rds_cluster_non_default_port.rds_cluster_non_default_port import ( + rds_cluster_non_default_port, + ) + + check = rds_cluster_non_default_port() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "RDS Cluster db-cluster-3 is using the default port 5432 for postgres." + ) + assert result[0].resource_id == "db-cluster-3" + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:cluster:db-cluster-3" + ) + assert result[0].resource_tags == [{"Key": "test", "Value": "test"}] + + @mock_aws + def test_rds_cluster_postgres_using_non_default_port(self): + conn = client("rds", region_name=AWS_REGION_US_EAST_1) + conn.create_db_cluster( + DBClusterIdentifier="db-cluster-4", + Engine="postgres", + StorageEncrypted=True, + DeletionProtection=True, + MasterUsername="cluster", + MasterUserPassword="password", + Port=5433, + Tags=[{"Key": "env", "Value": "production"}], + ) + + from prowler.providers.aws.services.rds.rds_service import RDS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_cluster_non_default_port.rds_cluster_non_default_port.rds_client", + new=RDS(aws_provider), + ): + from prowler.providers.aws.services.rds.rds_cluster_non_default_port.rds_cluster_non_default_port import ( + rds_cluster_non_default_port, + ) + + check = rds_cluster_non_default_port() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "RDS Cluster db-cluster-4 is not using the default port 5433 for postgres." + ) + assert result[0].resource_id == "db-cluster-4" + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:cluster:db-cluster-4" + ) + assert result[0].resource_tags == [ + {"Key": "env", "Value": "production"} + ] + + @mock_aws + def test_rds_cluster_aurora_mysql_default_port(self): + conn = client("rds", region_name=AWS_REGION_US_EAST_1) + conn.create_db_cluster( + DBClusterIdentifier="db-cluster-5", + Engine="aurora-mysql", + StorageEncrypted=True, + DeletionProtection=True, + MasterUsername="cluster", + MasterUserPassword="password", + Port=3306, + Tags=[{"Key": "env", "Value": "staging"}], + ) + + from prowler.providers.aws.services.rds.rds_service import RDS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_cluster_non_default_port.rds_cluster_non_default_port.rds_client", + new=RDS(aws_provider), + ): + from prowler.providers.aws.services.rds.rds_cluster_non_default_port.rds_cluster_non_default_port import ( + rds_cluster_non_default_port, + ) + + check = rds_cluster_non_default_port() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "RDS Cluster db-cluster-5 is using the default port 3306 for aurora-mysql." + ) + assert result[0].resource_id == "db-cluster-5" + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:cluster:db-cluster-5" + ) + assert result[0].resource_tags == [{"Key": "env", "Value": "staging"}] + + @mock_aws + def test_rds_cluster_aurora_mysql_non_default_port(self): + conn = client("rds", region_name=AWS_REGION_US_EAST_1) + conn.create_db_cluster( + DBClusterIdentifier="db-cluster-6", + Engine="aurora-mysql", + StorageEncrypted=True, + DeletionProtection=True, + MasterUsername="cluster", + MasterUserPassword="password", + Port=3307, + Tags=[{"Key": "env", "Value": "production"}], + ) + + from prowler.providers.aws.services.rds.rds_service import RDS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_cluster_non_default_port.rds_cluster_non_default_port.rds_client", + new=RDS(aws_provider), + ): + from prowler.providers.aws.services.rds.rds_cluster_non_default_port.rds_cluster_non_default_port import ( + rds_cluster_non_default_port, + ) + + check = rds_cluster_non_default_port() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "RDS Cluster db-cluster-6 is not using the default port 3307 for aurora-mysql." + ) + assert result[0].resource_id == "db-cluster-6" + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:cluster:db-cluster-6" ) assert result[0].resource_tags == [ {"Key": "env", "Value": "production"} @@ -134,7 +326,7 @@ class Test_rds_cluster_non_default_port: def test_rds_cluster_mysql_default_port(self): conn = client("rds", region_name=AWS_REGION_US_EAST_1) conn.create_db_cluster( - DBClusterIdentifier="db-cluster-1", + DBClusterIdentifier="db-cluster-7", Engine="mysql", StorageEncrypted=True, DeletionProtection=True, @@ -167,13 +359,13 @@ class Test_rds_cluster_non_default_port: assert result[0].status == "FAIL" assert ( result[0].status_extended - == "RDS Cluster db-cluster-1 is using the default port 3306 for mysql." + == "RDS Cluster db-cluster-7 is using the default port 3306 for mysql." ) - assert result[0].resource_id == "db-cluster-1" + assert result[0].resource_id == "db-cluster-7" assert result[0].region == AWS_REGION_US_EAST_1 assert ( result[0].resource_arn - == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:cluster:db-cluster-1" + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:cluster:db-cluster-7" ) assert result[0].resource_tags == [{"Key": "env", "Value": "staging"}] @@ -181,7 +373,7 @@ class Test_rds_cluster_non_default_port: def test_rds_cluster_mysql_non_default_port(self): conn = client("rds", region_name=AWS_REGION_US_EAST_1) conn.create_db_cluster( - DBClusterIdentifier="db-cluster-1", + DBClusterIdentifier="db-cluster-8", Engine="mysql", StorageEncrypted=True, DeletionProtection=True, @@ -214,13 +406,13 @@ class Test_rds_cluster_non_default_port: assert result[0].status == "PASS" assert ( result[0].status_extended - == "RDS Cluster db-cluster-1 is not using the default port 3307 for mysql." + == "RDS Cluster db-cluster-8 is not using the default port 3307 for mysql." ) - assert result[0].resource_id == "db-cluster-1" + assert result[0].resource_id == "db-cluster-8" assert result[0].region == AWS_REGION_US_EAST_1 assert ( result[0].resource_arn - == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:cluster:db-cluster-1" + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:cluster:db-cluster-8" ) assert result[0].resource_tags == [ {"Key": "env", "Value": "production"} diff --git a/tests/providers/aws/services/rds/rds_instance_non_default_port/rds_instance_non_default_port_test.py b/tests/providers/aws/services/rds/rds_instance_non_default_port/rds_instance_non_default_port_test.py index 97775dc847..fb2a356eef 100644 --- a/tests/providers/aws/services/rds/rds_instance_non_default_port/rds_instance_non_default_port_test.py +++ b/tests/providers/aws/services/rds/rds_instance_non_default_port/rds_instance_non_default_port_test.py @@ -35,11 +35,115 @@ class Test_rds_instance_non_default_port: assert len(result) == 0 @mock_aws - def test_rds_instance_using_default_port(self): + def test_rds_instance_aurora_postgres_using_default_port(self): conn = client("rds", region_name=AWS_REGION_US_EAST_1) conn.create_db_instance( DBInstanceIdentifier="db-master-1", AllocatedStorage=10, + Engine="aurora-postgresql", + DBName="staging-postgres", + DBInstanceClass="db.m1.small", + StorageEncrypted=True, + DeletionProtection=True, + PubliclyAccessible=True, + AutoMinorVersionUpgrade=True, + BackupRetentionPeriod=10, + Port=5432, + Tags=[{"Key": "test", "Value": "test"}], + ) + + from prowler.providers.aws.services.rds.rds_service import RDS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_instance_non_default_port.rds_instance_non_default_port.rds_client", + new=RDS(aws_provider), + ): + from prowler.providers.aws.services.rds.rds_instance_non_default_port.rds_instance_non_default_port import ( + rds_instance_non_default_port, + ) + + check = rds_instance_non_default_port() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "RDS Instance db-master-1 is using the default port 5432 for aurora-postgresql." + ) + assert result[0].resource_id == "db-master-1" + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-master-1" + ) + assert result[0].resource_tags == [{"Key": "test", "Value": "test"}] + + @mock_aws + def test_rds_instance_aurora_postgres_using_non_default_port(self): + conn = client("rds", region_name=AWS_REGION_US_EAST_1) + conn.create_db_instance( + DBInstanceIdentifier="db-master-2", + AllocatedStorage=10, + Engine="aurora-postgresql", + DBName="production-postgres", + DBInstanceClass="db.m1.small", + StorageEncrypted=True, + DeletionProtection=True, + PubliclyAccessible=True, + AutoMinorVersionUpgrade=True, + BackupRetentionPeriod=10, + Port=5433, + Tags=[{"Key": "env", "Value": "production"}], + ) + + from prowler.providers.aws.services.rds.rds_service import RDS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_instance_non_default_port.rds_instance_non_default_port.rds_client", + new=RDS(aws_provider), + ): + from prowler.providers.aws.services.rds.rds_instance_non_default_port.rds_instance_non_default_port import ( + rds_instance_non_default_port, + ) + + check = rds_instance_non_default_port() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "RDS Instance db-master-2 is not using the default port 5433 for aurora-postgresql." + ) + assert result[0].resource_id == "db-master-2" + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-master-2" + ) + assert result[0].resource_tags == [ + {"Key": "env", "Value": "production"} + ] + + @mock_aws + def test_rds_instance_postgres_using_default_port(self): + conn = client("rds", region_name=AWS_REGION_US_EAST_1) + conn.create_db_instance( + DBInstanceIdentifier="db-master-3", + AllocatedStorage=10, Engine="postgres", DBName="staging-postgres", DBInstanceClass="db.m1.small", @@ -75,21 +179,21 @@ class Test_rds_instance_non_default_port: assert result[0].status == "FAIL" assert ( result[0].status_extended - == "RDS Instance db-master-1 is using the default port 5432 for postgres." + == "RDS Instance db-master-3 is using the default port 5432 for postgres." ) - assert result[0].resource_id == "db-master-1" + assert result[0].resource_id == "db-master-3" assert result[0].region == AWS_REGION_US_EAST_1 assert ( result[0].resource_arn - == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-master-1" + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-master-3" ) assert result[0].resource_tags == [{"Key": "test", "Value": "test"}] @mock_aws - def test_rds_instance_using_non_default_port(self): + def test_rds_instance_postgres_using_non_default_port(self): conn = client("rds", region_name=AWS_REGION_US_EAST_1) conn.create_db_instance( - DBInstanceIdentifier="db-master-2", + DBInstanceIdentifier="db-master-4", AllocatedStorage=10, Engine="postgres", DBName="production-postgres", @@ -126,13 +230,221 @@ class Test_rds_instance_non_default_port: assert result[0].status == "PASS" assert ( result[0].status_extended - == "RDS Instance db-master-2 is not using the default port 5433 for postgres." + == "RDS Instance db-master-4 is not using the default port 5433 for postgres." ) - assert result[0].resource_id == "db-master-2" + assert result[0].resource_id == "db-master-4" assert result[0].region == AWS_REGION_US_EAST_1 assert ( result[0].resource_arn - == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-master-2" + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-master-4" + ) + assert result[0].resource_tags == [ + {"Key": "env", "Value": "production"} + ] + + @mock_aws + def test_rds_instance_mysql_default_port(self): + conn = client("rds", region_name=AWS_REGION_US_EAST_1) + conn.create_db_instance( + DBInstanceIdentifier="db-master-5", + AllocatedStorage=10, + Engine="mysql", + DBName="staging-mariadb", + DBInstanceClass="db.m1.small", + StorageEncrypted=True, + DeletionProtection=True, + PubliclyAccessible=True, + AutoMinorVersionUpgrade=True, + BackupRetentionPeriod=10, + Port=3306, + Tags=[{"Key": "env", "Value": "staging"}], + ) + + from prowler.providers.aws.services.rds.rds_service import RDS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_instance_non_default_port.rds_instance_non_default_port.rds_client", + new=RDS(aws_provider), + ): + from prowler.providers.aws.services.rds.rds_instance_non_default_port.rds_instance_non_default_port import ( + rds_instance_non_default_port, + ) + + check = rds_instance_non_default_port() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "RDS Instance db-master-5 is using the default port 3306 for mysql." + ) + assert result[0].resource_id == "db-master-5" + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-master-5" + ) + assert result[0].resource_tags == [{"Key": "env", "Value": "staging"}] + + @mock_aws + def test_rds_instance_mysql_non_default_port(self): + conn = client("rds", region_name=AWS_REGION_US_EAST_1) + conn.create_db_instance( + DBInstanceIdentifier="db-master-6", + AllocatedStorage=10, + Engine="mysql", + DBName="production-mariadb", + DBInstanceClass="db.m1.small", + StorageEncrypted=True, + DeletionProtection=True, + PubliclyAccessible=True, + AutoMinorVersionUpgrade=True, + BackupRetentionPeriod=10, + Port=3307, + Tags=[{"Key": "env", "Value": "production"}], + ) + + from prowler.providers.aws.services.rds.rds_service import RDS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_instance_non_default_port.rds_instance_non_default_port.rds_client", + new=RDS(aws_provider), + ): + from prowler.providers.aws.services.rds.rds_instance_non_default_port.rds_instance_non_default_port import ( + rds_instance_non_default_port, + ) + + check = rds_instance_non_default_port() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "RDS Instance db-master-6 is not using the default port 3307 for mysql." + ) + assert result[0].resource_id == "db-master-6" + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-master-6" + ) + assert result[0].resource_tags == [ + {"Key": "env", "Value": "production"} + ] + + @mock_aws + def test_rds_instance_aurora_mysql_default_port(self): + conn = client("rds", region_name=AWS_REGION_US_EAST_1) + conn.create_db_instance( + DBInstanceIdentifier="db-master-7", + AllocatedStorage=10, + Engine="aurora-mysql", + DBName="staging-mariadb", + DBInstanceClass="db.m1.small", + StorageEncrypted=True, + DeletionProtection=True, + PubliclyAccessible=True, + AutoMinorVersionUpgrade=True, + BackupRetentionPeriod=10, + Port=3306, + Tags=[{"Key": "env", "Value": "staging"}], + ) + + from prowler.providers.aws.services.rds.rds_service import RDS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_instance_non_default_port.rds_instance_non_default_port.rds_client", + new=RDS(aws_provider), + ): + from prowler.providers.aws.services.rds.rds_instance_non_default_port.rds_instance_non_default_port import ( + rds_instance_non_default_port, + ) + + check = rds_instance_non_default_port() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "RDS Instance db-master-7 is using the default port 3306 for aurora-mysql." + ) + assert result[0].resource_id == "db-master-7" + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-master-7" + ) + assert result[0].resource_tags == [{"Key": "env", "Value": "staging"}] + + @mock_aws + def test_rds_instance_aurora_mysql_non_default_port(self): + conn = client("rds", region_name=AWS_REGION_US_EAST_1) + conn.create_db_instance( + DBInstanceIdentifier="db-master-8", + AllocatedStorage=10, + Engine="aurora-mysql", + DBName="production-mariadb", + DBInstanceClass="db.m1.small", + StorageEncrypted=True, + DeletionProtection=True, + PubliclyAccessible=True, + AutoMinorVersionUpgrade=True, + BackupRetentionPeriod=10, + Port=3307, + Tags=[{"Key": "env", "Value": "production"}], + ) + + from prowler.providers.aws.services.rds.rds_service import RDS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_instance_non_default_port.rds_instance_non_default_port.rds_client", + new=RDS(aws_provider), + ): + from prowler.providers.aws.services.rds.rds_instance_non_default_port.rds_instance_non_default_port import ( + rds_instance_non_default_port, + ) + + check = rds_instance_non_default_port() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "RDS Instance db-master-8 is not using the default port 3307 for aurora-mysql." + ) + assert result[0].resource_id == "db-master-8" + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-master-8" ) assert result[0].resource_tags == [ {"Key": "env", "Value": "production"} @@ -142,7 +454,7 @@ class Test_rds_instance_non_default_port: def test_rds_instance_mariadb_default_port(self): conn = client("rds", region_name=AWS_REGION_US_EAST_1) conn.create_db_instance( - DBInstanceIdentifier="db-master-3", + DBInstanceIdentifier="db-master-9", AllocatedStorage=10, Engine="mariadb", DBName="staging-mariadb", @@ -179,13 +491,13 @@ class Test_rds_instance_non_default_port: assert result[0].status == "FAIL" assert ( result[0].status_extended - == "RDS Instance db-master-3 is using the default port 3306 for mariadb." + == "RDS Instance db-master-9 is using the default port 3306 for mariadb." ) - assert result[0].resource_id == "db-master-3" + assert result[0].resource_id == "db-master-9" assert result[0].region == AWS_REGION_US_EAST_1 assert ( result[0].resource_arn - == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-master-3" + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-master-9" ) assert result[0].resource_tags == [{"Key": "env", "Value": "staging"}] @@ -193,7 +505,7 @@ class Test_rds_instance_non_default_port: def test_rds_instance_mariadb_non_default_port(self): conn = client("rds", region_name=AWS_REGION_US_EAST_1) conn.create_db_instance( - DBInstanceIdentifier="db-master-4", + DBInstanceIdentifier="db-master-10", AllocatedStorage=10, Engine="mariadb", DBName="production-mariadb", @@ -230,13 +542,13 @@ class Test_rds_instance_non_default_port: assert result[0].status == "PASS" assert ( result[0].status_extended - == "RDS Instance db-master-4 is not using the default port 3307 for mariadb." + == "RDS Instance db-master-10 is not using the default port 3307 for mariadb." ) - assert result[0].resource_id == "db-master-4" + assert result[0].resource_id == "db-master-10" assert result[0].region == AWS_REGION_US_EAST_1 assert ( result[0].resource_arn - == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-master-4" + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-master-10" ) assert result[0].resource_tags == [ {"Key": "env", "Value": "production"} From 971e73f9cbf5d978c2278f19de0717a92bd9ab24 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 09:09:51 -0400 Subject: [PATCH 55/67] chore(deps): bump google-api-python-client from 2.154.0 to 2.155.0 (#6155) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 13 +++++++++---- pyproject.toml | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index a949279fe0..ec69ef1e6e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1719,13 +1719,13 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] [[package]] name = "google-api-python-client" -version = "2.154.0" +version = "2.155.0" description = "Google API Client Library for Python" optional = false python-versions = ">=3.7" files = [ - {file = "google_api_python_client-2.154.0-py2.py3-none-any.whl", hash = "sha256:a521bbbb2ec0ba9d6f307cdd64ed6e21eeac372d1bd7493a4ab5022941f784ad"}, - {file = "google_api_python_client-2.154.0.tar.gz", hash = "sha256:1b420062e03bfcaa1c79e2e00a612d29a6a934151ceb3d272fe150a656dc8f17"}, + {file = "google_api_python_client-2.155.0-py2.py3-none-any.whl", hash = "sha256:83fe9b5aa4160899079d7c93a37be306546a17e6686e2549bcc9584f1a229747"}, + {file = "google_api_python_client-2.155.0.tar.gz", hash = "sha256:25529f89f0d13abcf3c05c089c423fb2858ac16e0b3727543393468d0d7af67c"}, ] [package.dependencies] @@ -4458,6 +4458,7 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6"}, @@ -4466,6 +4467,7 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632"}, @@ -4474,6 +4476,7 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a"}, @@ -4482,6 +4485,7 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:fc4b630cd3fa2cf7fce38afa91d7cfe844a9f75d7f0f36393fa98815e911d987"}, @@ -4490,6 +4494,7 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2f1c3765db32be59d18ab3953f43ab62a761327aafc1594a2a1fbe038b8b8a7"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d85252669dc32f98ebcd5d36768f5d4faeaeaa2d655ac0473be490ecdae3c285"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e143ada795c341b56de9418c58d028989093ee611aa27ffb9b7f609c00d813ed"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2c59aa6170b990d8d2719323e628aaf36f3bfbc1c26279c0eeeb24d05d2d11c7"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win32.whl", hash = "sha256:beffaed67936fbbeffd10966a4eb53c402fafd3d6833770516bf7314bc6ffa12"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win_amd64.whl", hash = "sha256:040ae85536960525ea62868b642bdb0c2cc6021c9f9d507810c0c604e66f5a7b"}, {file = "ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f"}, @@ -5194,4 +5199,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "ea4e52926a0b10ccc9109beaa54ac3fb3c02090dd8bd0dc127f303d28f7f3894" +content-hash = "a61586cca241af652de0cab655487e7f8ff44af790b0063346143dd7967a7119" diff --git a/pyproject.toml b/pyproject.toml index 69279330ca..211ee90358 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,7 +55,7 @@ cryptography = "43.0.1" dash = "2.18.2" dash-bootstrap-components = "1.6.0" detect-secrets = "1.5.0" -google-api-python-client = "2.154.0" +google-api-python-client = "2.155.0" google-auth-httplib2 = ">=0.1,<0.3" jsonschema = "4.23.0" kubernetes = "31.0.0" From 5920cdc48ffa9da5a15b860422376922a589242a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 09:10:20 -0400 Subject: [PATCH 56/67] chore(deps): bump trufflesecurity/trufflehog from 3.86.0 to 3.86.1 (#6156) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/find-secrets.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/find-secrets.yml b/.github/workflows/find-secrets.yml index c7ca25f34c..372cdf3f16 100644 --- a/.github/workflows/find-secrets.yml +++ b/.github/workflows/find-secrets.yml @@ -11,7 +11,7 @@ jobs: with: fetch-depth: 0 - name: TruffleHog OSS - uses: trufflesecurity/trufflehog@v3.86.0 + uses: trufflesecurity/trufflehog@v3.86.1 with: path: ./ base: ${{ github.event.repository.default_branch }} From 997831e33d2464dbc0beef7b3da7246a69bf7b3d Mon Sep 17 00:00:00 2001 From: Prowler Bot Date: Thu, 12 Dec 2024 14:10:46 +0100 Subject: [PATCH 57/67] chore(regions_update): Changes in regions for AWS services (#6158) Co-authored-by: MrCloudSec <38561120+MrCloudSec@users.noreply.github.com> --- prowler/providers/aws/aws_regions_by_service.json | 1 + 1 file changed, 1 insertion(+) diff --git a/prowler/providers/aws/aws_regions_by_service.json b/prowler/providers/aws/aws_regions_by_service.json index a0b21fc92c..cea65047cd 100644 --- a/prowler/providers/aws/aws_regions_by_service.json +++ b/prowler/providers/aws/aws_regions_by_service.json @@ -7315,6 +7315,7 @@ "ap-southeast-2", "ap-southeast-3", "ap-southeast-4", + "ap-southeast-5", "ca-central-1", "ca-west-1", "eu-central-1", From 6e595eaf92c04a150ff00f6ac49625f7cc114db6 Mon Sep 17 00:00:00 2001 From: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com> Date: Thu, 12 Dec 2024 14:22:56 +0100 Subject: [PATCH 58/67] feat(ec2): add new fixer `ec2_instance_port_cifs_exposed_to_internet_fixer` (#6159) --- ...nce_port_cifs_exposed_to_internet_fixer.py | 51 +++ ...ort_cifs_exposed_to_internet_fixer_test.py | 430 ++++++++++++++++++ 2 files changed, 481 insertions(+) create mode 100644 prowler/providers/aws/services/ec2/ec2_instance_port_cifs_exposed_to_internet/ec2_instance_port_cifs_exposed_to_internet_fixer.py create mode 100644 tests/providers/aws/services/ec2/ec2_instance_port_cifs_exposed_to_internet/ec2_instance_port_cifs_exposed_to_internet_fixer_test.py diff --git a/prowler/providers/aws/services/ec2/ec2_instance_port_cifs_exposed_to_internet/ec2_instance_port_cifs_exposed_to_internet_fixer.py b/prowler/providers/aws/services/ec2/ec2_instance_port_cifs_exposed_to_internet/ec2_instance_port_cifs_exposed_to_internet_fixer.py new file mode 100644 index 0000000000..f898b8e5f3 --- /dev/null +++ b/prowler/providers/aws/services/ec2/ec2_instance_port_cifs_exposed_to_internet/ec2_instance_port_cifs_exposed_to_internet_fixer.py @@ -0,0 +1,51 @@ +from prowler.lib.logger import logger +from prowler.providers.aws.services.ec2.ec2_client import ec2_client +from prowler.providers.aws.services.ec2.lib.security_groups import check_security_group + + +def fixer(resource_id: str, region: str) -> bool: + """ + Revokes any ingress rule allowing CIFS ports (139, 445) from any address (0.0.0.0/0) + for the EC2 instance's security groups. + This fixer will only be triggered if the check identifies CIFS ports open to the Internet. + Requires the ec2:RevokeSecurityGroupIngress permission. + Permissions: + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "ec2:RevokeSecurityGroupIngress", + "Resource": "*" + } + ] + } + Args: + resource_id (str): The EC2 instance ID. + region (str): The AWS region where the EC2 instance exists. + Returns: + bool: True if the operation is successful (ingress rule revoked), False otherwise. + """ + try: + regional_client = ec2_client.regional_clients[region] + check_ports = [139, 445] + for instance in ec2_client.instances: + if instance.id == resource_id: + for sg in ec2_client.security_groups.values(): + if sg.id in instance.security_groups: + for ingress_rule in sg.ingress_rules: + if check_security_group( + ingress_rule, "tcp", check_ports, any_address=True + ): + regional_client.revoke_security_group_ingress( + GroupId=sg.id, + IpPermissions=[ingress_rule], + ) + + except Exception as error: + logger.error( + f"{region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return False + else: + return True diff --git a/tests/providers/aws/services/ec2/ec2_instance_port_cifs_exposed_to_internet/ec2_instance_port_cifs_exposed_to_internet_fixer_test.py b/tests/providers/aws/services/ec2/ec2_instance_port_cifs_exposed_to_internet/ec2_instance_port_cifs_exposed_to_internet_fixer_test.py new file mode 100644 index 0000000000..ef7771637e --- /dev/null +++ b/tests/providers/aws/services/ec2/ec2_instance_port_cifs_exposed_to_internet/ec2_instance_port_cifs_exposed_to_internet_fixer_test.py @@ -0,0 +1,430 @@ +from unittest import mock + +import botocore +import botocore.client +from boto3 import client, resource +from moto import mock_aws + +from tests.providers.aws.utils import AWS_REGION_EU_WEST_1, set_mocked_aws_provider + +mock_make_api_call = botocore.client.BaseClient._make_api_call + + +def mock_make_api_call_error(self, operation_name, kwarg): + if operation_name == "RevokeSecurityGroupIngress": + raise botocore.exceptions.ClientError( + { + "Error": { + "Code": "InvalidPermission.NotFound", + "Message": "The specified rule does not exist in this security group.", + } + }, + operation_name, + ) + return mock_make_api_call(self, operation_name, kwarg) + + +class Test_ec2_instance_port_cifs_exposed_to_internet_fixer: + @mock_aws + def test_ec2_instance_exposed_port_in_private_subnet_with_ip4_and_ip6(self): + # Create EC2 Mocked Resources + ec2_client = client("ec2", region_name=AWS_REGION_EU_WEST_1) + ec2_resource = resource("ec2", region_name=AWS_REGION_EU_WEST_1) + vpc_id = ec2_client.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"]["VpcId"] + default_sg = ec2_client.describe_security_groups(GroupNames=["default"])[ + "SecurityGroups" + ][0] + default_sg_id = default_sg["GroupId"] + ec2_client.authorize_security_group_ingress( + GroupId=default_sg_id, + IpPermissions=[ + { + "IpProtocol": "tcp", + "FromPort": 1, + "ToPort": 1000, + "IpRanges": [{"CidrIp": "0.0.0.0/0"}, {"CidrIp": "10.0.0.0/24"}], + "Ipv6Ranges": [{"CidrIpv6": "::/0"}, {"CidrIpv6": "2001:db8::/32"}], + }, + { + "IpProtocol": "tcp", + "FromPort": 445, + "ToPort": 445, + "IpRanges": [{"CidrIp": "0.0.0.0/0"}, {"CidrIp": "10.0.0.0/24"}], + "Ipv6Ranges": [{"CidrIpv6": "::/0"}, {"CidrIpv6": "2001:db8::/32"}], + }, + ], + ) + subnet_id = ec2_client.create_subnet(VpcId=vpc_id, CidrBlock="10.0.0.0/16")[ + "Subnet" + ]["SubnetId"] + instance_id = ec2_resource.create_instances( + ImageId="ami-12345678", + MinCount=1, + MaxCount=1, + InstanceType="t2.micro", + SecurityGroupIds=[default_sg_id], + SubnetId=subnet_id, + TagSpecifications=[ + {"ResourceType": "instance", "Tags": [{"Key": "Name", "Value": "test"}]} + ], + )[0].id + + from prowler.providers.aws.services.ec2.ec2_service import EC2 + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.ec2.ec2_instance_port_cifs_exposed_to_internet.ec2_instance_port_cifs_exposed_to_internet_fixer.ec2_client", + new=EC2(aws_provider), + ): + # Test Fixer + from prowler.providers.aws.services.ec2.ec2_instance_port_cifs_exposed_to_internet.ec2_instance_port_cifs_exposed_to_internet_fixer import ( + fixer, + ) + + assert fixer(instance_id, AWS_REGION_EU_WEST_1) + + @mock_aws + def test_ec2_instance_exposed_port_error(self): + with mock.patch( + "botocore.client.BaseClient._make_api_call", new=mock_make_api_call_error + ): + # Create EC2 Mocked Resources + ec2_client = client("ec2", region_name=AWS_REGION_EU_WEST_1) + ec2_resource = resource("ec2", region_name=AWS_REGION_EU_WEST_1) + vpc_id = ec2_client.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"]["VpcId"] + default_sg = ec2_client.describe_security_groups(GroupNames=["default"])[ + "SecurityGroups" + ][0] + default_sg_id = default_sg["GroupId"] + ec2_client.authorize_security_group_ingress( + GroupId=default_sg_id, + IpPermissions=[ + { + "IpProtocol": "tcp", + "FromPort": 1, + "ToPort": 1000, + "IpRanges": [ + {"CidrIp": "0.0.0.0/0"}, + {"CidrIp": "10.0.0.0/24"}, + ], + "Ipv6Ranges": [ + {"CidrIpv6": "::/0"}, + {"CidrIpv6": "2001:db8::/32"}, + ], + }, + { + "IpProtocol": "tcp", + "FromPort": 445, + "ToPort": 445, + "IpRanges": [ + {"CidrIp": "0.0.0.0/0"}, + {"CidrIp": "10.0.0.0/24"}, + ], + "Ipv6Ranges": [ + {"CidrIpv6": "::/0"}, + {"CidrIpv6": "2001:db8::/32"}, + ], + }, + ], + ) + subnet_id = ec2_client.create_subnet(VpcId=vpc_id, CidrBlock="10.0.0.0/16")[ + "Subnet" + ]["SubnetId"] + instance_id = ec2_resource.create_instances( + ImageId="ami-12345678", + MinCount=1, + MaxCount=1, + InstanceType="t2.micro", + SecurityGroupIds=[default_sg_id], + SubnetId=subnet_id, + TagSpecifications=[ + { + "ResourceType": "instance", + "Tags": [{"Key": "Name", "Value": "test"}], + } + ], + )[0].id + + from prowler.providers.aws.services.ec2.ec2_service import EC2 + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.ec2.ec2_instance_port_cifs_exposed_to_internet.ec2_instance_port_cifs_exposed_to_internet_fixer.ec2_client", + new=EC2(aws_provider), + ): + # Test Fixer + from prowler.providers.aws.services.ec2.ec2_instance_port_cifs_exposed_to_internet.ec2_instance_port_cifs_exposed_to_internet_fixer import ( + fixer, + ) + + assert not fixer(instance_id, AWS_REGION_EU_WEST_1) + + @mock_aws + def test_ec2_instance_exposed_port_in_private_subnet_only_with_ip4(self): + # Create EC2 Mocked Resources + ec2_client = client("ec2", region_name=AWS_REGION_EU_WEST_1) + ec2_resource = resource("ec2", region_name=AWS_REGION_EU_WEST_1) + vpc_id = ec2_client.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"]["VpcId"] + default_sg = ec2_client.describe_security_groups(GroupNames=["default"])[ + "SecurityGroups" + ][0] + default_sg_id = default_sg["GroupId"] + ec2_client.authorize_security_group_ingress( + GroupId=default_sg_id, + IpPermissions=[ + { + "IpProtocol": "tcp", + "FromPort": 139, + "ToPort": 139, + "IpRanges": [{"CidrIp": "0.0.0.0/0"}, {"CidrIp": "10.0.0.0/24"}], + }, + { + "IpProtocol": "tcp", + "FromPort": 445, + "ToPort": 445, + "IpRanges": [{"CidrIp": "0.0.0.0/0"}, {"CidrIp": "10.0.0.0/24"}], + }, + ], + ) + subnet_id = ec2_client.create_subnet(VpcId=vpc_id, CidrBlock="10.0.0.0/16")[ + "Subnet" + ]["SubnetId"] + instance_id = ec2_resource.create_instances( + ImageId="ami-12345678", + MinCount=1, + MaxCount=1, + InstanceType="t2.micro", + SecurityGroupIds=[default_sg_id], + SubnetId=subnet_id, + TagSpecifications=[ + {"ResourceType": "instance", "Tags": [{"Key": "Name", "Value": "test"}]} + ], + )[0].id + + from prowler.providers.aws.services.ec2.ec2_service import EC2 + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.ec2.ec2_instance_port_cifs_exposed_to_internet.ec2_instance_port_cifs_exposed_to_internet_fixer.ec2_client", + new=EC2(aws_provider), + ): + # Test Fixer + from prowler.providers.aws.services.ec2.ec2_instance_port_cifs_exposed_to_internet.ec2_instance_port_cifs_exposed_to_internet_fixer import ( + fixer, + ) + + assert fixer(instance_id, AWS_REGION_EU_WEST_1) + + @mock_aws + def test_ec2_instance_exposed_port_in_private_subnet_only_with_ip6(self): + # Create EC2 Mocked Resources + ec2_client = client("ec2", region_name=AWS_REGION_EU_WEST_1) + ec2_resource = resource("ec2", region_name=AWS_REGION_EU_WEST_1) + vpc_id = ec2_client.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"]["VpcId"] + default_sg = ec2_client.describe_security_groups(GroupNames=["default"])[ + "SecurityGroups" + ][0] + default_sg_id = default_sg["GroupId"] + ec2_client.authorize_security_group_ingress( + GroupId=default_sg_id, + IpPermissions=[ + { + "IpProtocol": "tcp", + "FromPort": 139, + "ToPort": 139, + "Ipv6Ranges": [{"CidrIpv6": "::/0"}, {"CidrIpv6": "2001:db8::/32"}], + }, + { + "IpProtocol": "tcp", + "FromPort": 445, + "ToPort": 445, + "Ipv6Ranges": [{"CidrIpv6": "::/0"}, {"CidrIpv6": "2001:db8::/32"}], + }, + ], + ) + subnet_id = ec2_client.create_subnet(VpcId=vpc_id, CidrBlock="10.0.0.0/16")[ + "Subnet" + ]["SubnetId"] + instance_id = ec2_resource.create_instances( + ImageId="ami-12345678", + MinCount=1, + MaxCount=1, + InstanceType="t2.micro", + SecurityGroupIds=[default_sg_id], + SubnetId=subnet_id, + TagSpecifications=[ + {"ResourceType": "instance", "Tags": [{"Key": "Name", "Value": "test"}]} + ], + )[0].id + + from prowler.providers.aws.services.ec2.ec2_service import EC2 + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.ec2.ec2_instance_port_cifs_exposed_to_internet.ec2_instance_port_cifs_exposed_to_internet_fixer.ec2_client", + new=EC2(aws_provider), + ): + # Test Fixer + from prowler.providers.aws.services.ec2.ec2_instance_port_cifs_exposed_to_internet.ec2_instance_port_cifs_exposed_to_internet_fixer import ( + fixer, + ) + + assert fixer(instance_id, AWS_REGION_EU_WEST_1) + + @mock_aws + def test_ec2_instance_exposed_port_in_public_subnet_only_139_port(self): + # Create EC2 Mocked Resources + ec2_client = client("ec2", region_name=AWS_REGION_EU_WEST_1) + ec2_resource = resource("ec2", region_name=AWS_REGION_EU_WEST_1) + vpc_id = ec2_client.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"]["VpcId"] + default_sg = ec2_client.describe_security_groups(GroupNames=["default"])[ + "SecurityGroups" + ][0] + default_sg_id = default_sg["GroupId"] + ec2_client.authorize_security_group_ingress( + GroupId=default_sg_id, + IpPermissions=[ + { + "IpProtocol": "tcp", + "FromPort": 139, + "ToPort": 139, + "IpRanges": [{"CidrIp": "0.0.0.0/0"}], + } + ], + ) + subnet_id = ec2_client.create_subnet(VpcId=vpc_id, CidrBlock="10.0.0.0/16")[ + "Subnet" + ]["SubnetId"] + instance = ec2_resource.create_instances( + ImageId="ami-12345678", + MinCount=1, + MaxCount=1, + InstanceType="t2.micro", + SecurityGroupIds=[default_sg_id], + NetworkInterfaces=[ + { + "DeviceIndex": 0, + "SubnetId": subnet_id, + "AssociatePublicIpAddress": True, + } + ], + TagSpecifications=[ + {"ResourceType": "instance", "Tags": [{"Key": "Name", "Value": "test"}]} + ], + )[0] + + from prowler.providers.aws.services.ec2.ec2_service import EC2 + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.ec2.ec2_instance_port_cifs_exposed_to_internet.ec2_instance_port_cifs_exposed_to_internet_fixer.ec2_client", + new=EC2(aws_provider), + ): + # Test Fixer + from prowler.providers.aws.services.ec2.ec2_instance_port_cifs_exposed_to_internet.ec2_instance_port_cifs_exposed_to_internet_fixer import ( + fixer, + ) + + assert fixer(instance.id, AWS_REGION_EU_WEST_1) + + @mock_aws + def test_ec2_instance_exposed_port_with_public_ip_in_public_subnet_only_445_port( + self, + ): + # Create EC2 Mocked Resources + ec2_client = client("ec2", region_name=AWS_REGION_EU_WEST_1) + ec2_resource = resource("ec2", region_name=AWS_REGION_EU_WEST_1) + vpc_id = ec2_client.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"]["VpcId"] + default_sg = ec2_client.describe_security_groups(GroupNames=["default"])[ + "SecurityGroups" + ][0] + default_sg_id = default_sg["GroupId"] + ec2_client.authorize_security_group_ingress( + GroupId=default_sg_id, + IpPermissions=[ + { + "IpProtocol": "tcp", + "FromPort": 445, + "ToPort": 445, + "IpRanges": [{"CidrIp": "0.0.0.0/0"}], + } + ], + ) + subnet_id = ec2_client.create_subnet(VpcId=vpc_id, CidrBlock="10.0.0.0/16")[ + "Subnet" + ]["SubnetId"] + # add default route of subnet to an internet gateway to make it public + igw_id = ec2_client.create_internet_gateway()["InternetGateway"][ + "InternetGatewayId" + ] + # attach internet gateway to subnet + ec2_client.attach_internet_gateway(InternetGatewayId=igw_id, VpcId=vpc_id) + # create route table + route_table_id = ec2_client.create_route_table(VpcId=vpc_id)["RouteTable"][ + "RouteTableId" + ] + # associate route table with subnet + ec2_client.associate_route_table( + RouteTableId=route_table_id, SubnetId=subnet_id + ) + # add route to route table + ec2_client.create_route( + RouteTableId=route_table_id, + DestinationCidrBlock="0.0.0.0/0", + GatewayId=igw_id, + ) + instance = ec2_resource.create_instances( + ImageId="ami-12345678", + MinCount=1, + MaxCount=1, + InstanceType="t2.micro", + SecurityGroupIds=[default_sg_id], + NetworkInterfaces=[ + { + "DeviceIndex": 0, + "SubnetId": subnet_id, + "AssociatePublicIpAddress": True, + } + ], + TagSpecifications=[ + {"ResourceType": "instance", "Tags": [{"Key": "Name", "Value": "test"}]} + ], + )[0] + + from prowler.providers.aws.services.ec2.ec2_service import EC2 + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.ec2.ec2_instance_port_cifs_exposed_to_internet.ec2_instance_port_cifs_exposed_to_internet_fixer.ec2_client", + new=EC2(aws_provider), + ): + # Test Fixer + from prowler.providers.aws.services.ec2.ec2_instance_port_cifs_exposed_to_internet.ec2_instance_port_cifs_exposed_to_internet_fixer import ( + fixer, + ) + + assert fixer(instance.id, AWS_REGION_EU_WEST_1) From 657310dc25fc2b27d2780a777e493f496c6e055e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 10:39:22 -0400 Subject: [PATCH 59/67] chore(deps): bump boto3 from 1.35.77 to 1.35.78 (#6154) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 10 +++++----- pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index ec69ef1e6e..4e5419389e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -775,17 +775,17 @@ files = [ [[package]] name = "boto3" -version = "1.35.77" +version = "1.35.78" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" files = [ - {file = "boto3-1.35.77-py3-none-any.whl", hash = "sha256:a09871805f8e462349a1c33c23eb413668df0bf68424e61d53518e1a7d883b2f"}, - {file = "boto3-1.35.77.tar.gz", hash = "sha256:cc819cdbccbc2d0dc185f1dcfe74cf3809489c4cae63c2e5d6a557aa0c5ab928"}, + {file = "boto3-1.35.78-py3-none-any.whl", hash = "sha256:5ef7166fe5060637b92af8dc152cd7acecf96b3fc9c5456706a886cadb534391"}, + {file = "boto3-1.35.78.tar.gz", hash = "sha256:fc8001519c8842e766ad3793bde3fbd0bb39e821a582fc12cf67876b8f3cf7f1"}, ] [package.dependencies] -botocore = ">=1.35.77,<1.36.0" +botocore = ">=1.35.78,<1.36.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.10.0,<0.11.0" @@ -5199,4 +5199,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "a61586cca241af652de0cab655487e7f8ff44af790b0063346143dd7967a7119" +content-hash = "fc7ab70dbf60c40b45d1e8e712f6e0f54701d50df1d82d64b6b72eed862654a8" diff --git a/pyproject.toml b/pyproject.toml index 211ee90358..8a63574233 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ azure-mgmt-storage = "21.2.1" azure-mgmt-subscription = "3.1.1" azure-mgmt-web = "7.3.1" azure-storage-blob = "12.24.0" -boto3 = "1.35.77" +boto3 = "1.35.78" botocore = "1.35.78" colorama = "0.4.6" cryptography = "43.0.1" From 26c70976c09734b7de491168c896f2102b7e2d92 Mon Sep 17 00:00:00 2001 From: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com> Date: Thu, 12 Dec 2024 16:10:43 +0100 Subject: [PATCH 60/67] feat(s3): add new fixer `s3_bucket_public_write_acl_fixer` (#5855) --- .../s3_bucket_public_write_acl_fixer.py | 36 ++ .../s3_bucket_public_write_acl_fixer_test.py | 432 ++++++++++++++++++ 2 files changed, 468 insertions(+) create mode 100644 prowler/providers/aws/services/s3/s3_bucket_public_write_acl/s3_bucket_public_write_acl_fixer.py create mode 100644 tests/providers/aws/services/s3/s3_bucket_public_write_acl/s3_bucket_public_write_acl_fixer_test.py diff --git a/prowler/providers/aws/services/s3/s3_bucket_public_write_acl/s3_bucket_public_write_acl_fixer.py b/prowler/providers/aws/services/s3/s3_bucket_public_write_acl/s3_bucket_public_write_acl_fixer.py new file mode 100644 index 0000000000..e27787a967 --- /dev/null +++ b/prowler/providers/aws/services/s3/s3_bucket_public_write_acl/s3_bucket_public_write_acl_fixer.py @@ -0,0 +1,36 @@ +from prowler.lib.logger import logger +from prowler.providers.aws.services.s3.s3_client import s3_client + + +def fixer(resource_id: str, region: str) -> bool: + """ + Modify the S3 bucket ACL to restrict public write access. + Specifically, this fixer sets the ACL of the bucket to 'private' to prevent + public write access to the S3 bucket. Requires the s3:PutBucketAcl permission. + Permissions: + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "s3:PutBucketAcl", + "Resource": "*" + } + ] + } + Args: + resource_id (str): The S3 bucket id. + region (str): AWS region where the S3 bucket exists. + Returns: + bool: True if the operation is successful (bucket access is updated), False otherwise. + """ + try: + regional_client = s3_client.regional_clients[region] + regional_client.put_bucket_acl(Bucket=resource_id, ACL="private") + except Exception as error: + logger.error( + f"{region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return False + else: + return True diff --git a/tests/providers/aws/services/s3/s3_bucket_public_write_acl/s3_bucket_public_write_acl_fixer_test.py b/tests/providers/aws/services/s3/s3_bucket_public_write_acl/s3_bucket_public_write_acl_fixer_test.py new file mode 100644 index 0000000000..768ad12f79 --- /dev/null +++ b/tests/providers/aws/services/s3/s3_bucket_public_write_acl/s3_bucket_public_write_acl_fixer_test.py @@ -0,0 +1,432 @@ +from unittest import mock + +from boto3 import client +from moto import mock_aws + +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_US_EAST_1, + set_mocked_aws_provider, +) + + +class Test_s3_bucket_public_write_acl_fixer: + @mock_aws + def test_bucket_public_write_ACL_AllUsers_WRITE(self): + s3_client = client("s3", region_name=AWS_REGION_US_EAST_1) + bucket_name_us = "bucket_test_us" + s3_client.create_bucket(Bucket=bucket_name_us) + bucket_owner = s3_client.get_bucket_acl(Bucket=bucket_name_us)["Owner"] + # Generate S3Control Client + s3control_client = client("s3control", region_name=AWS_REGION_US_EAST_1) + s3control_client.put_public_access_block( + AccountId=AWS_ACCOUNT_NUMBER, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + s3_client.put_public_access_block( + Bucket=bucket_name_us, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + s3_client.put_bucket_acl( + Bucket=bucket_name_us, + AccessControlPolicy={ + "Grants": [ + { + "Grantee": { + "URI": "http://acs.amazonaws.com/groups/global/AllUsers", + "Type": "Group", + }, + "Permission": "WRITE", + }, + ], + "Owner": bucket_owner, + }, + ) + from prowler.providers.aws.services.s3.s3_service import S3 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_public_write_acl.s3_bucket_public_write_acl_fixer.s3_client", + new=S3(aws_provider), + ): + # Test Fixer + from prowler.providers.aws.services.s3.s3_bucket_public_write_acl.s3_bucket_public_write_acl_fixer import ( + fixer, + ) + + assert fixer(bucket_name_us, AWS_REGION_US_EAST_1) + + @mock_aws + def test_bucket_public_write_ACL_AllUsers_WRITE_ACP(self): + s3_client = client("s3", region_name=AWS_REGION_US_EAST_1) + bucket_name_us = "bucket_test_us" + s3_client.create_bucket(Bucket=bucket_name_us) + bucket_owner = s3_client.get_bucket_acl(Bucket=bucket_name_us)["Owner"] + # Generate S3Control Client + s3control_client = client("s3control", region_name=AWS_REGION_US_EAST_1) + s3control_client.put_public_access_block( + AccountId=AWS_ACCOUNT_NUMBER, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + s3_client.put_public_access_block( + Bucket=bucket_name_us, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + s3_client.put_bucket_acl( + Bucket=bucket_name_us, + AccessControlPolicy={ + "Grants": [ + { + "Grantee": { + "URI": "http://acs.amazonaws.com/groups/global/AllUsers", + "Type": "Group", + }, + "Permission": "WRITE_ACP", + }, + ], + "Owner": bucket_owner, + }, + ) + from prowler.providers.aws.services.s3.s3_service import S3 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_public_write_acl.s3_bucket_public_write_acl_fixer.s3_client", + new=S3(aws_provider), + ): + # Test Fixer + from prowler.providers.aws.services.s3.s3_bucket_public_write_acl.s3_bucket_public_write_acl_fixer import ( + fixer, + ) + + assert fixer(bucket_name_us, AWS_REGION_US_EAST_1) + + @mock_aws + def test_bucket_public_write_ACL_AllUsers_FULL_CONTROL(self): + s3_client = client("s3", region_name=AWS_REGION_US_EAST_1) + bucket_name_us = "bucket_test_us" + s3_client.create_bucket(Bucket=bucket_name_us) + bucket_owner = s3_client.get_bucket_acl(Bucket=bucket_name_us)["Owner"] + # Generate S3Control Client + s3control_client = client("s3control", region_name=AWS_REGION_US_EAST_1) + s3control_client.put_public_access_block( + AccountId=AWS_ACCOUNT_NUMBER, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + s3_client.put_public_access_block( + Bucket=bucket_name_us, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + s3_client.put_bucket_acl( + Bucket=bucket_name_us, + AccessControlPolicy={ + "Grants": [ + { + "Grantee": { + "URI": "http://acs.amazonaws.com/groups/global/AllUsers", + "Type": "Group", + }, + "Permission": "FULL_CONTROL", + }, + ], + "Owner": bucket_owner, + }, + ) + from prowler.providers.aws.services.s3.s3_service import S3 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_public_write_acl.s3_bucket_public_write_acl_fixer.s3_client", + new=S3(aws_provider), + ): + # Test Fixer + from prowler.providers.aws.services.s3.s3_bucket_public_write_acl.s3_bucket_public_write_acl_fixer import ( + fixer, + ) + + assert fixer(bucket_name_us, AWS_REGION_US_EAST_1) + + @mock_aws + def test_bucket_public_write_ACL_AuthenticatedUsers_WRITE(self): + s3_client = client("s3", region_name=AWS_REGION_US_EAST_1) + bucket_name_us = "bucket_test_us" + s3_client.create_bucket(Bucket=bucket_name_us) + bucket_owner = s3_client.get_bucket_acl(Bucket=bucket_name_us)["Owner"] + # Generate S3Control Client + s3control_client = client("s3control", region_name=AWS_REGION_US_EAST_1) + s3control_client.put_public_access_block( + AccountId=AWS_ACCOUNT_NUMBER, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + s3_client.put_public_access_block( + Bucket=bucket_name_us, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + s3_client.put_bucket_acl( + Bucket=bucket_name_us, + AccessControlPolicy={ + "Grants": [ + { + "Grantee": { + "URI": "http://acs.amazonaws.com/groups/global/AuthenticatedUsers", + "Type": "Group", + }, + "Permission": "WRITE", + }, + ], + "Owner": bucket_owner, + }, + ) + from prowler.providers.aws.services.s3.s3_service import S3 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_public_write_acl.s3_bucket_public_write_acl_fixer.s3_client", + new=S3(aws_provider), + ): + # Test Fixer + from prowler.providers.aws.services.s3.s3_bucket_public_write_acl.s3_bucket_public_write_acl_fixer import ( + fixer, + ) + + assert fixer(bucket_name_us, AWS_REGION_US_EAST_1) + + @mock_aws + def test_bucket_public_write_ACL_AuthenticatedUsers_WRITE_ACP(self): + s3_client = client("s3", region_name=AWS_REGION_US_EAST_1) + bucket_name_us = "bucket_test_us" + s3_client.create_bucket(Bucket=bucket_name_us) + bucket_owner = s3_client.get_bucket_acl(Bucket=bucket_name_us)["Owner"] + # Generate S3Control Client + s3control_client = client("s3control", region_name=AWS_REGION_US_EAST_1) + s3control_client.put_public_access_block( + AccountId=AWS_ACCOUNT_NUMBER, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + s3_client.put_public_access_block( + Bucket=bucket_name_us, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + s3_client.put_bucket_acl( + Bucket=bucket_name_us, + AccessControlPolicy={ + "Grants": [ + { + "Grantee": { + "URI": "http://acs.amazonaws.com/groups/global/AuthenticatedUsers", + "Type": "Group", + }, + "Permission": "WRITE_ACP", + }, + ], + "Owner": bucket_owner, + }, + ) + from prowler.providers.aws.services.s3.s3_service import S3 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_public_write_acl.s3_bucket_public_write_acl_fixer.s3_client", + new=S3(aws_provider), + ): + # Test Fixer + from prowler.providers.aws.services.s3.s3_bucket_public_write_acl.s3_bucket_public_write_acl_fixer import ( + fixer, + ) + + assert fixer(bucket_name_us, AWS_REGION_US_EAST_1) + + @mock_aws + def test_bucket_public_write_ACL_AuthenticatedUsers_FULL_CONTROL(self): + s3_client = client("s3", region_name=AWS_REGION_US_EAST_1) + bucket_name_us = "bucket_test_us" + s3_client.create_bucket(Bucket=bucket_name_us) + bucket_owner = s3_client.get_bucket_acl(Bucket=bucket_name_us)["Owner"] + # Generate S3Control Client + s3control_client = client("s3control", region_name=AWS_REGION_US_EAST_1) + s3control_client.put_public_access_block( + AccountId=AWS_ACCOUNT_NUMBER, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + s3_client.put_public_access_block( + Bucket=bucket_name_us, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + s3_client.put_bucket_acl( + Bucket=bucket_name_us, + AccessControlPolicy={ + "Grants": [ + { + "Grantee": { + "URI": "http://acs.amazonaws.com/groups/global/AuthenticatedUsers", + "Type": "Group", + }, + "Permission": "FULL_CONTROL", + }, + ], + "Owner": bucket_owner, + }, + ) + from prowler.providers.aws.services.s3.s3_service import S3 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_public_write_acl.s3_bucket_public_write_acl_fixer.s3_client", + new=S3(aws_provider), + ): + # Test Fixer + from prowler.providers.aws.services.s3.s3_bucket_public_write_acl.s3_bucket_public_write_acl_fixer import ( + fixer, + ) + + assert fixer(bucket_name_us, AWS_REGION_US_EAST_1) + + @mock_aws + def test_bucket_public_write_ACL_AllUsers_WRITE_error(self): + s3_client = client("s3", region_name=AWS_REGION_US_EAST_1) + bucket_name_us = "bucket_test_us" + s3_client.create_bucket(Bucket=bucket_name_us) + bucket_owner = s3_client.get_bucket_acl(Bucket=bucket_name_us)["Owner"] + # Generate S3Control Client + s3control_client = client("s3control", region_name=AWS_REGION_US_EAST_1) + s3control_client.put_public_access_block( + AccountId=AWS_ACCOUNT_NUMBER, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + s3_client.put_public_access_block( + Bucket=bucket_name_us, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + s3_client.put_bucket_acl( + Bucket=bucket_name_us, + AccessControlPolicy={ + "Grants": [ + { + "Grantee": { + "URI": "http://acs.amazonaws.com/groups/global/AllUsers", + "Type": "Group", + }, + "Permission": "WRITE", + }, + ], + "Owner": bucket_owner, + }, + ) + from prowler.providers.aws.services.s3.s3_service import S3 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_public_write_acl.s3_bucket_public_write_acl_fixer.s3_client", + new=S3(aws_provider), + ): + # Test Fixer + from prowler.providers.aws.services.s3.s3_bucket_public_write_acl.s3_bucket_public_write_acl_fixer import ( + fixer, + ) + + assert not fixer("bucket_name_us_non_existing", AWS_REGION_US_EAST_1) From a90c77282780ad440ec76a3079a38ac78e479070 Mon Sep 17 00:00:00 2001 From: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com> Date: Thu, 12 Dec 2024 16:16:46 +0100 Subject: [PATCH 61/67] feat(s3): add new fixer `s3_bucket_public_list_acl_fixer` (#6166) --- .../s3_bucket_public_list_acl_fixer.py | 40 ++ .../s3_bucket_public_list_acl_fixer_test.py | 432 ++++++++++++++++++ 2 files changed, 472 insertions(+) create mode 100644 prowler/providers/aws/services/s3/s3_bucket_public_list_acl/s3_bucket_public_list_acl_fixer.py create mode 100644 tests/providers/aws/services/s3/s3_bucket_public_list_acl/s3_bucket_public_list_acl_fixer_test.py diff --git a/prowler/providers/aws/services/s3/s3_bucket_public_list_acl/s3_bucket_public_list_acl_fixer.py b/prowler/providers/aws/services/s3/s3_bucket_public_list_acl/s3_bucket_public_list_acl_fixer.py new file mode 100644 index 0000000000..1ab315b61d --- /dev/null +++ b/prowler/providers/aws/services/s3/s3_bucket_public_list_acl/s3_bucket_public_list_acl_fixer.py @@ -0,0 +1,40 @@ +from prowler.lib.logger import logger +from prowler.providers.aws.services.s3.s3_client import s3_client + + +def fixer(resource_id: str, region: str) -> bool: + """ + Modify the S3 bucket ACL to restrict public read access. + Specifically, this fixer sets the ACL of the bucket to 'private' to prevent + any public access to the S3 bucket. + Requires the s3:PutBucketAcl permission. + + Permissions: + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "s3:PutBucketAcl", + "Resource": "*" + } + ] + } + + Args: + resource_id (str): The S3 bucket name. + region (str): AWS region where the S3 bucket exists. + + Returns: + bool: True if the operation is successful (bucket access is updated), False otherwise. + """ + try: + regional_client = s3_client.regional_clients[region] + regional_client.put_bucket_acl(Bucket=resource_id, ACL="private") + except Exception as error: + logger.error( + f"{region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return False + else: + return True diff --git a/tests/providers/aws/services/s3/s3_bucket_public_list_acl/s3_bucket_public_list_acl_fixer_test.py b/tests/providers/aws/services/s3/s3_bucket_public_list_acl/s3_bucket_public_list_acl_fixer_test.py new file mode 100644 index 0000000000..6f7a8f2414 --- /dev/null +++ b/tests/providers/aws/services/s3/s3_bucket_public_list_acl/s3_bucket_public_list_acl_fixer_test.py @@ -0,0 +1,432 @@ +from unittest import mock + +from boto3 import client +from moto import mock_aws + +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_US_EAST_1, + set_mocked_aws_provider, +) + + +class Test_s3_bucket_public_list_acl_fixer: + @mock_aws + def test_bucket_public_list_ACL_AllUsers_READ(self): + s3_client = client("s3", region_name=AWS_REGION_US_EAST_1) + bucket_name_us = "bucket_test_us" + s3_client.create_bucket(Bucket=bucket_name_us) + bucket_owner = s3_client.get_bucket_acl(Bucket=bucket_name_us)["Owner"] + # Generate S3Control Client + s3control_client = client("s3control", region_name=AWS_REGION_US_EAST_1) + s3control_client.put_public_access_block( + AccountId=AWS_ACCOUNT_NUMBER, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + s3_client.put_public_access_block( + Bucket=bucket_name_us, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + s3_client.put_bucket_acl( + Bucket=bucket_name_us, + AccessControlPolicy={ + "Grants": [ + { + "Grantee": { + "URI": "http://acs.amazonaws.com/groups/global/AllUsers", + "Type": "Group", + }, + "Permission": "READ", + }, + ], + "Owner": bucket_owner, + }, + ) + from prowler.providers.aws.services.s3.s3_service import S3 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_public_list_acl.s3_bucket_public_list_acl_fixer.s3_client", + new=S3(aws_provider), + ): + # Test Fixer + from prowler.providers.aws.services.s3.s3_bucket_public_list_acl.s3_bucket_public_list_acl_fixer import ( + fixer, + ) + + assert fixer(bucket_name_us, AWS_REGION_US_EAST_1) + + @mock_aws + def test_bucket_public_list_ACL_AllUsers_READ_ACP(self): + s3_client = client("s3", region_name=AWS_REGION_US_EAST_1) + bucket_name_us = "bucket_test_us" + s3_client.create_bucket(Bucket=bucket_name_us) + bucket_owner = s3_client.get_bucket_acl(Bucket=bucket_name_us)["Owner"] + # Generate S3Control Client + s3control_client = client("s3control", region_name=AWS_REGION_US_EAST_1) + s3control_client.put_public_access_block( + AccountId=AWS_ACCOUNT_NUMBER, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + s3_client.put_public_access_block( + Bucket=bucket_name_us, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + s3_client.put_bucket_acl( + Bucket=bucket_name_us, + AccessControlPolicy={ + "Grants": [ + { + "Grantee": { + "URI": "http://acs.amazonaws.com/groups/global/AllUsers", + "Type": "Group", + }, + "Permission": "READ_ACP", + }, + ], + "Owner": bucket_owner, + }, + ) + from prowler.providers.aws.services.s3.s3_service import S3 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_public_list_acl.s3_bucket_public_list_acl_fixer.s3_client", + new=S3(aws_provider), + ): + # Test Fixer + from prowler.providers.aws.services.s3.s3_bucket_public_list_acl.s3_bucket_public_list_acl_fixer import ( + fixer, + ) + + assert fixer(bucket_name_us, AWS_REGION_US_EAST_1) + + @mock_aws + def test_bucket_public_list_ACL_AllUsers_FULL_CONTROL(self): + s3_client = client("s3", region_name=AWS_REGION_US_EAST_1) + bucket_name_us = "bucket_test_us" + s3_client.create_bucket(Bucket=bucket_name_us) + bucket_owner = s3_client.get_bucket_acl(Bucket=bucket_name_us)["Owner"] + # Generate S3Control Client + s3control_client = client("s3control", region_name=AWS_REGION_US_EAST_1) + s3control_client.put_public_access_block( + AccountId=AWS_ACCOUNT_NUMBER, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + s3_client.put_public_access_block( + Bucket=bucket_name_us, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + s3_client.put_bucket_acl( + Bucket=bucket_name_us, + AccessControlPolicy={ + "Grants": [ + { + "Grantee": { + "URI": "http://acs.amazonaws.com/groups/global/AllUsers", + "Type": "Group", + }, + "Permission": "FULL_CONTROL", + }, + ], + "Owner": bucket_owner, + }, + ) + from prowler.providers.aws.services.s3.s3_service import S3 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_public_list_acl.s3_bucket_public_list_acl_fixer.s3_client", + new=S3(aws_provider), + ): + # Test Fixer + from prowler.providers.aws.services.s3.s3_bucket_public_list_acl.s3_bucket_public_list_acl_fixer import ( + fixer, + ) + + assert fixer(bucket_name_us, AWS_REGION_US_EAST_1) + + @mock_aws + def test_bucket_public_list_ACL_AuthenticatedUsers_READ(self): + s3_client = client("s3", region_name=AWS_REGION_US_EAST_1) + bucket_name_us = "bucket_test_us" + s3_client.create_bucket(Bucket=bucket_name_us) + bucket_owner = s3_client.get_bucket_acl(Bucket=bucket_name_us)["Owner"] + # Generate S3Control Client + s3control_client = client("s3control", region_name=AWS_REGION_US_EAST_1) + s3control_client.put_public_access_block( + AccountId=AWS_ACCOUNT_NUMBER, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + s3_client.put_public_access_block( + Bucket=bucket_name_us, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + s3_client.put_bucket_acl( + Bucket=bucket_name_us, + AccessControlPolicy={ + "Grants": [ + { + "Grantee": { + "URI": "http://acs.amazonaws.com/groups/global/AuthenticatedUsers", + "Type": "Group", + }, + "Permission": "READ", + }, + ], + "Owner": bucket_owner, + }, + ) + from prowler.providers.aws.services.s3.s3_service import S3 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_public_list_acl.s3_bucket_public_list_acl_fixer.s3_client", + new=S3(aws_provider), + ): + # Test Fixer + from prowler.providers.aws.services.s3.s3_bucket_public_list_acl.s3_bucket_public_list_acl_fixer import ( + fixer, + ) + + assert fixer(bucket_name_us, AWS_REGION_US_EAST_1) + + @mock_aws + def test_bucket_public_list_ACL_AuthenticatedUsers_READ_ACP(self): + s3_client = client("s3", region_name=AWS_REGION_US_EAST_1) + bucket_name_us = "bucket_test_us" + s3_client.create_bucket(Bucket=bucket_name_us) + bucket_owner = s3_client.get_bucket_acl(Bucket=bucket_name_us)["Owner"] + # Generate S3Control Client + s3control_client = client("s3control", region_name=AWS_REGION_US_EAST_1) + s3control_client.put_public_access_block( + AccountId=AWS_ACCOUNT_NUMBER, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + s3_client.put_public_access_block( + Bucket=bucket_name_us, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + s3_client.put_bucket_acl( + Bucket=bucket_name_us, + AccessControlPolicy={ + "Grants": [ + { + "Grantee": { + "URI": "http://acs.amazonaws.com/groups/global/AuthenticatedUsers", + "Type": "Group", + }, + "Permission": "READ_ACP", + }, + ], + "Owner": bucket_owner, + }, + ) + from prowler.providers.aws.services.s3.s3_service import S3 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_public_list_acl.s3_bucket_public_list_acl_fixer.s3_client", + new=S3(aws_provider), + ): + # Test Fixer + from prowler.providers.aws.services.s3.s3_bucket_public_list_acl.s3_bucket_public_list_acl_fixer import ( + fixer, + ) + + assert fixer(bucket_name_us, AWS_REGION_US_EAST_1) + + @mock_aws + def test_bucket_public_list_ACL_AuthenticatedUsers_FULL_CONTROL(self): + s3_client = client("s3", region_name=AWS_REGION_US_EAST_1) + bucket_name_us = "bucket_test_us" + s3_client.create_bucket(Bucket=bucket_name_us) + bucket_owner = s3_client.get_bucket_acl(Bucket=bucket_name_us)["Owner"] + # Generate S3Control Client + s3control_client = client("s3control", region_name=AWS_REGION_US_EAST_1) + s3control_client.put_public_access_block( + AccountId=AWS_ACCOUNT_NUMBER, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + s3_client.put_public_access_block( + Bucket=bucket_name_us, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + s3_client.put_bucket_acl( + Bucket=bucket_name_us, + AccessControlPolicy={ + "Grants": [ + { + "Grantee": { + "URI": "http://acs.amazonaws.com/groups/global/AuthenticatedUsers", + "Type": "Group", + }, + "Permission": "FULL_CONTROL", + }, + ], + "Owner": bucket_owner, + }, + ) + from prowler.providers.aws.services.s3.s3_service import S3 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_public_list_acl.s3_bucket_public_list_acl_fixer.s3_client", + new=S3(aws_provider), + ): + # Test Fixer + from prowler.providers.aws.services.s3.s3_bucket_public_list_acl.s3_bucket_public_list_acl_fixer import ( + fixer, + ) + + assert fixer(bucket_name_us, AWS_REGION_US_EAST_1) + + @mock_aws + def test_bucket_public_list_ACL_AllUsers_READ_error(self): + s3_client = client("s3", region_name=AWS_REGION_US_EAST_1) + bucket_name_us = "bucket_test_us" + s3_client.create_bucket(Bucket=bucket_name_us) + bucket_owner = s3_client.get_bucket_acl(Bucket=bucket_name_us)["Owner"] + # Generate S3Control Client + s3control_client = client("s3control", region_name=AWS_REGION_US_EAST_1) + s3control_client.put_public_access_block( + AccountId=AWS_ACCOUNT_NUMBER, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + s3_client.put_public_access_block( + Bucket=bucket_name_us, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + s3_client.put_bucket_acl( + Bucket=bucket_name_us, + AccessControlPolicy={ + "Grants": [ + { + "Grantee": { + "URI": "http://acs.amazonaws.com/groups/global/AllUsers", + "Type": "Group", + }, + "Permission": "READ", + }, + ], + "Owner": bucket_owner, + }, + ) + from prowler.providers.aws.services.s3.s3_service import S3 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_public_list_acl.s3_bucket_public_list_acl_fixer.s3_client", + new=S3(aws_provider), + ): + # Test Fixer + from prowler.providers.aws.services.s3.s3_bucket_public_list_acl.s3_bucket_public_list_acl_fixer import ( + fixer, + ) + + assert not fixer("bucket_name_us_non_existing", AWS_REGION_US_EAST_1) From be347b2428fd65fc90830ce65bef1d92a4c37120 Mon Sep 17 00:00:00 2001 From: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com> Date: Thu, 12 Dec 2024 16:27:20 +0100 Subject: [PATCH 62/67] feat(ec2): add new check `ec2_launch_template_imdsv2_required` (#6139) Co-authored-by: MrCloudSec --- .../__init__.py | 0 ...nch_template_imdsv2_required.metadata.json | 32 +++ .../ec2_launch_template_imdsv2_required.py | 42 ++++ .../providers/aws/services/ec2/ec2_service.py | 9 +- ...c2_launch_template_imdsv2_required_test.py | 195 ++++++++++++++++++ .../aws/services/ec2/ec2_service_test.py | 6 + 6 files changed, 283 insertions(+), 1 deletion(-) create mode 100644 prowler/providers/aws/services/ec2/ec2_launch_template_imdsv2_required/__init__.py create mode 100644 prowler/providers/aws/services/ec2/ec2_launch_template_imdsv2_required/ec2_launch_template_imdsv2_required.metadata.json create mode 100644 prowler/providers/aws/services/ec2/ec2_launch_template_imdsv2_required/ec2_launch_template_imdsv2_required.py create mode 100644 tests/providers/aws/services/ec2/ec2_launch_template_imdsv2_required/ec2_launch_template_imdsv2_required_test.py diff --git a/prowler/providers/aws/services/ec2/ec2_launch_template_imdsv2_required/__init__.py b/prowler/providers/aws/services/ec2/ec2_launch_template_imdsv2_required/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/ec2/ec2_launch_template_imdsv2_required/ec2_launch_template_imdsv2_required.metadata.json b/prowler/providers/aws/services/ec2/ec2_launch_template_imdsv2_required/ec2_launch_template_imdsv2_required.metadata.json new file mode 100644 index 0000000000..591225a7ce --- /dev/null +++ b/prowler/providers/aws/services/ec2/ec2_launch_template_imdsv2_required/ec2_launch_template_imdsv2_required.metadata.json @@ -0,0 +1,32 @@ +{ + "Provider": "aws", + "CheckID": "ec2_launch_template_imdsv2_required", + "CheckTitle": "Amazon EC2 launch templates should have IMDSv2 enabled and required.", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "ec2", + "SubServiceName": "", + "ResourceIdTemplate": "arn:aws:ec2:region:account-id:launch-template/resource-id", + "Severity": "high", + "ResourceType": "AwsEc2LaunchTemplate", + "Description": "This control checks if Amazon EC2 launch templates are configured with IMDSv2 enabled and required. The control fails if IMDSv2 is not enabled or required in the launch template versions.", + "Risk": "Without IMDSv2 required, EC2 instances may be vulnerable to metadata service attacks, allowing unauthorized access to instance metadata, potentially leading to compromise of instance credentials or other sensitive data.", + "RelatedUrl": "https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html", + "Remediation": { + "Code": { + "CLI": "aws ec2 modify-launch-template --launch-template-id --version --metadata-options HttpTokens=required", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/ec2-controls.html#ec2-170", + "Terraform": "" + }, + "Recommendation": { + "Text": "To ensure EC2 launch templates have IMDSv2 enabled and required, update the template to configure the Instance Metadata Service Version 2 as required.", + "Url": "https://docs.aws.amazon.com/autoscaling/ec2/userguide/create-launch-template.html#change-metadata-options" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/ec2/ec2_launch_template_imdsv2_required/ec2_launch_template_imdsv2_required.py b/prowler/providers/aws/services/ec2/ec2_launch_template_imdsv2_required/ec2_launch_template_imdsv2_required.py new file mode 100644 index 0000000000..377d7519ed --- /dev/null +++ b/prowler/providers/aws/services/ec2/ec2_launch_template_imdsv2_required/ec2_launch_template_imdsv2_required.py @@ -0,0 +1,42 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.ec2.ec2_client import ec2_client + + +class ec2_launch_template_imdsv2_required(Check): + def execute(self): + findings = [] + for template in ec2_client.launch_templates: + report = Check_Report_AWS(self.metadata()) + report.region = template.region + report.resource_id = template.id + report.resource_arn = template.arn + report.resource_tags = template.tags + + versions_with_imdsv2_required = [] + versions_with_metadata_disabled = [] + versions_with_no_imdsv2 = [] + + for version in template.versions: + if ( + version.template_data.http_endpoint == "enabled" + and version.template_data.http_tokens == "required" + ): + versions_with_imdsv2_required.append(str(version.version_number)) + elif version.template_data.http_endpoint == "disabled": + versions_with_metadata_disabled.append(str(version.version_number)) + else: + versions_with_no_imdsv2.append(str(version.version_number)) + + if versions_with_imdsv2_required: + report.status = "PASS" + report.status_extended = f"EC2 Launch Template {template.name} has IMDSv2 enabled and required in the following versions: {', '.join(versions_with_imdsv2_required)}." + elif versions_with_metadata_disabled: + report.status = "PASS" + report.status_extended = f"EC2 Launch Template {template.name} has metadata service disabled in the following versions: {', '.join(versions_with_metadata_disabled)}." + else: + report.status = "FAIL" + report.status_extended = f"EC2 Launch Template {template.name} has IMDSv2 disabled or not required in the following versions: {', '.join(versions_with_no_imdsv2)}." + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/ec2/ec2_service.py b/prowler/providers/aws/services/ec2/ec2_service.py index 4c695c3110..cd559128f8 100644 --- a/prowler/providers/aws/services/ec2/ec2_service.py +++ b/prowler/providers/aws/services/ec2/ec2_service.py @@ -10,7 +10,6 @@ from prowler.lib.scan_filters.scan_filters import is_resource_filtered from prowler.providers.aws.lib.service.service import AWSService -################## EC2 class EC2(AWSService): def __init__(self, provider): # Call AWSService's __init__ @@ -569,6 +568,12 @@ class EC2(AWSService): ), network_interfaces=enis, associate_public_ip_address=associate_public_ip, + http_tokens=template_version["LaunchTemplateData"] + .get("MetadataOptions", {}) + .get("HttpTokens", ""), + http_endpoint=template_version["LaunchTemplateData"] + .get("MetadataOptions", {}) + .get("HttpEndpoint", ""), ), ) ) @@ -763,6 +768,8 @@ class TemplateData(BaseModel): user_data: str network_interfaces: Optional[list[NetworkInterface]] associate_public_ip_address: Optional[bool] + http_tokens: Optional[str] + http_endpoint: Optional[str] class LaunchTemplateVersion(BaseModel): diff --git a/tests/providers/aws/services/ec2/ec2_launch_template_imdsv2_required/ec2_launch_template_imdsv2_required_test.py b/tests/providers/aws/services/ec2/ec2_launch_template_imdsv2_required/ec2_launch_template_imdsv2_required_test.py new file mode 100644 index 0000000000..22ff04f349 --- /dev/null +++ b/tests/providers/aws/services/ec2/ec2_launch_template_imdsv2_required/ec2_launch_template_imdsv2_required_test.py @@ -0,0 +1,195 @@ +from unittest import mock + +import botocore +from boto3 import client +from moto import mock_aws + +from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider + +make_api_call = botocore.client.BaseClient._make_api_call + + +def mock_make_api_call(self, operation_name, kwarg): + if operation_name == "DescribeLaunchTemplateVersions": + return { + "LaunchTemplateVersions": [ + { + "VersionNumber": 1, + "LaunchTemplateData": { + "MetadataOptions": { + "HttpEndpoint": "enabled", + "HttpTokens": "required", + } + }, + } + ] + } + return make_api_call(self, operation_name, kwarg) + + +def mock_make_api_call_not_required(self, operation_name, kwarg): + if operation_name == "DescribeLaunchTemplateVersions": + return { + "LaunchTemplateVersions": [ + { + "VersionNumber": 1, + "LaunchTemplateData": { + "MetadataOptions": { + "HttpEndpoint": "enabled", + "HttpTokens": "optional", + } + }, + } + ] + } + return make_api_call(self, operation_name, kwarg) + + +class Test_ec2_launch_template_imdsv2_required: + @mock_aws + def test_no_launch_templates(self): + ec2_client = client("ec2", region_name=AWS_REGION_US_EAST_1) + ec2_client.launch_templates = [] + + from prowler.providers.aws.services.ec2.ec2_service import EC2 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.ec2.ec2_launch_template_imdsv2_required.ec2_launch_template_imdsv2_required.ec2_client", + new=EC2(aws_provider), + ), + ): + # Test Check + from prowler.providers.aws.services.ec2.ec2_launch_template_imdsv2_required.ec2_launch_template_imdsv2_required import ( + ec2_launch_template_imdsv2_required, + ) + + check = ec2_launch_template_imdsv2_required() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_launch_template_imdsv2_required(self): + with mock.patch( + "botocore.client.BaseClient._make_api_call", new=mock_make_api_call + ): + launch_template_name = "test-imdsv2-required" + ec2_client = client("ec2", region_name=AWS_REGION_US_EAST_1) + ec2_client.create_launch_template( + LaunchTemplateName=launch_template_name, + VersionDescription="Launch Template with IMDSv2 required", + LaunchTemplateData={ + "InstanceType": "t1.micro", + "MetadataOptions": { + "HttpEndpoint": "enabled", + "HttpTokens": "required", + }, + }, + ) + + launch_template_id = ec2_client.describe_launch_templates( + LaunchTemplateNames=[launch_template_name] + )["LaunchTemplates"][0]["LaunchTemplateId"] + + from prowler.providers.aws.services.ec2.ec2_service import EC2 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.ec2.ec2_launch_template_imdsv2_required.ec2_launch_template_imdsv2_required.ec2_client", + new=EC2(aws_provider), + ), + ): + # Test Check + from prowler.providers.aws.services.ec2.ec2_launch_template_imdsv2_required.ec2_launch_template_imdsv2_required import ( + ec2_launch_template_imdsv2_required, + ) + + check = ec2_launch_template_imdsv2_required() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"EC2 Launch Template {launch_template_name} has IMDSv2 enabled and required in the following versions: 1." + ) + assert result[0].resource_id == launch_template_id + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:ec2:{AWS_REGION_US_EAST_1}:123456789012:launch-template/{launch_template_id}" + ) + assert result[0].resource_tags == [] + + @mock_aws + def test_launch_template_imdsv2_not_required(self): + with mock.patch( + "botocore.client.BaseClient._make_api_call", + new=mock_make_api_call_not_required, + ): + ec2_client = client("ec2", region_name=AWS_REGION_US_EAST_1) + launch_template_name = "test-imdsv2-not-required" + ec2_client.create_launch_template( + LaunchTemplateName=launch_template_name, + VersionDescription="Launch Template without IMDSv2 required", + LaunchTemplateData={ + "InstanceType": "t1.micro", + "MetadataOptions": { + "HttpEndpoint": "enabled", + "HttpTokens": "optional", + }, + }, + ) + + launch_template_id = ec2_client.describe_launch_templates( + LaunchTemplateNames=[launch_template_name] + )["LaunchTemplates"][0]["LaunchTemplateId"] + + from prowler.providers.aws.services.ec2.ec2_service import EC2 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.ec2.ec2_launch_template_imdsv2_required.ec2_launch_template_imdsv2_required.ec2_client", + new=EC2(aws_provider), + ), + ): + # Test Check + from prowler.providers.aws.services.ec2.ec2_launch_template_imdsv2_required.ec2_launch_template_imdsv2_required import ( + ec2_launch_template_imdsv2_required, + ) + + check = ec2_launch_template_imdsv2_required() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"EC2 Launch Template {launch_template_name} has IMDSv2 disabled or not required in the following versions: 1." + ) + assert result[0].resource_id == launch_template_id + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:ec2:{AWS_REGION_US_EAST_1}:123456789012:launch-template/{launch_template_id}" + ) + assert result[0].resource_tags == [] diff --git a/tests/providers/aws/services/ec2/ec2_service_test.py b/tests/providers/aws/services/ec2/ec2_service_test.py index f4fd93b14a..629e319f38 100644 --- a/tests/providers/aws/services/ec2/ec2_service_test.py +++ b/tests/providers/aws/services/ec2/ec2_service_test.py @@ -36,6 +36,10 @@ def mock_make_api_call(self, operation_name, kwarg): "foobar123".encode(encoding_format_utf_8) ).decode(encoding_format_utf_8), "NetworkInterfaces": [{"AssociatePublicIpAddress": True}], + "MetadataOptions": { + "HttpEndpoint": "enabled", + "HttpTokens": "optional", + }, }, } ] @@ -741,6 +745,8 @@ class Test_EC2_Service: ) assert version.template_data.associate_public_ip_address + assert version.template_data.http_endpoint == "enabled" + assert version.template_data.http_tokens == "optional" # Test EC2 Describe VPN Endpoints @mock.patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) From 316eb049dde7e34993fade7cf918c3f0a72f4210 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 11:29:23 -0400 Subject: [PATCH 63/67] chore(deps): bump botocore from 1.35.78 to 1.35.79 (#6153) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index 4e5419389e..c48363bcb1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -794,13 +794,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.35.78" +version = "1.35.79" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" files = [ - {file = "botocore-1.35.78-py3-none-any.whl", hash = "sha256:41c37bd7c0326f25122f33ec84fb80fc0a14d7fcc9961431b0e57568e88c9cb5"}, - {file = "botocore-1.35.78.tar.gz", hash = "sha256:6905036c25449ae8dba5e950e4b908e4b8a6fe6b516bf61e007ecb62fa21f323"}, + {file = "botocore-1.35.79-py3-none-any.whl", hash = "sha256:e6b10bb9a357e3f5ca2e60f6dd15a85d311b9a476eb21b3c0c2a3b364a2897c8"}, + {file = "botocore-1.35.79.tar.gz", hash = "sha256:245bfdda1b1508539ddd1819c67a8a2cc81780adf0715d3de418d64c4247f346"}, ] [package.dependencies] @@ -5199,4 +5199,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "fc7ab70dbf60c40b45d1e8e712f6e0f54701d50df1d82d64b6b72eed862654a8" +content-hash = "e00da6013a01923ac8e79017e7fdb221e09a3dcf581ad8d74e39550be64cc2f3" diff --git a/pyproject.toml b/pyproject.toml index 8a63574233..5220cc87c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ azure-mgmt-subscription = "3.1.1" azure-mgmt-web = "7.3.1" azure-storage-blob = "12.24.0" boto3 = "1.35.78" -botocore = "1.35.78" +botocore = "1.35.79" colorama = "0.4.6" cryptography = "43.0.1" dash = "2.18.2" From 83c204e010f3c803b9e26b3d42acbf9d9d06ea23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Mart=C3=ADn?= Date: Thu, 12 Dec 2024 16:51:09 +0100 Subject: [PATCH 64/67] fix(rds): add invalid SG to status_extended (#6157) --- .../rds_instance_no_public_access.py | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/prowler/providers/aws/services/rds/rds_instance_no_public_access/rds_instance_no_public_access.py b/prowler/providers/aws/services/rds/rds_instance_no_public_access/rds_instance_no_public_access.py index ca0e1b3beb..dffa850693 100644 --- a/prowler/providers/aws/services/rds/rds_instance_no_public_access/rds_instance_no_public_access.py +++ b/prowler/providers/aws/services/rds/rds_instance_no_public_access/rds_instance_no_public_access.py @@ -37,18 +37,21 @@ class rds_instance_no_public_access(Check): ): report.status_extended = f"RDS Instance {db_instance.id} is set as publicly accessible and security group {security_group.name} ({security_group.id}) has {db_instance.engine} port {db_instance_port} open to the Internet at endpoint {db_instance.endpoint.get('Address')} but is not in a public subnet." public_sg = True + if db_instance.subnet_ids: + for subnet_id in db_instance.subnet_ids: + if ( + subnet_id in vpc_client.vpc_subnets + and vpc_client.vpc_subnets[ + subnet_id + ].public + ): + report.status = "FAIL" + report.status_extended = f"RDS Instance {db_instance.id} is set as publicly accessible and security group {security_group.name} ({security_group.id}) has {db_instance.engine} port {db_instance_port} open to the Internet at endpoint {db_instance.endpoint.get('Address')} in a public subnet {subnet_id}." + break + if public_sg: break if public_sg: break - if db_instance.subnet_ids: - for subnet_id in db_instance.subnet_ids: - if ( - subnet_id in vpc_client.vpc_subnets - and vpc_client.vpc_subnets[subnet_id].public - ): - report.status = "FAIL" - report.status_extended = f"RDS Instance {db_instance.id} is set as publicly accessible and security group {security_group.name} ({security_group.id}) has {db_instance.engine} port {db_instance_port} open to the Internet at endpoint {db_instance.endpoint.get('Address')} in a public subnet {subnet_id}." - break findings.append(report) From 7d2c50991b16a8502cc13db0a185a6fa6ec79a36 Mon Sep 17 00:00:00 2001 From: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com> Date: Thu, 12 Dec 2024 17:17:41 +0100 Subject: [PATCH 65/67] feat(s3): add new fixer `s3_bucket_public_access_fixer` (#6164) Co-authored-by: MrCloudSec --- .../s3_bucket_public_access_fixer.py | 46 +++++ .../s3_bucket_public_access_fixer_test.py | 169 ++++++++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 prowler/providers/aws/services/s3/s3_bucket_public_access/s3_bucket_public_access_fixer.py create mode 100644 tests/providers/aws/services/s3/s3_bucket_public_access/s3_bucket_public_access_fixer_test.py diff --git a/prowler/providers/aws/services/s3/s3_bucket_public_access/s3_bucket_public_access_fixer.py b/prowler/providers/aws/services/s3/s3_bucket_public_access/s3_bucket_public_access_fixer.py new file mode 100644 index 0000000000..693336dec9 --- /dev/null +++ b/prowler/providers/aws/services/s3/s3_bucket_public_access/s3_bucket_public_access_fixer.py @@ -0,0 +1,46 @@ +from prowler.lib.logger import logger +from prowler.providers.aws.services.s3.s3_client import s3_client + + +def fixer(resource_id: str, region: str) -> bool: + """ + Modify the S3 bucket's public access settings to block all public access. + Specifically, this fixer configures the bucket's public access block settings to + prevent any public access (ACLs and policies). Requires the s3:PutBucketPublicAccessBlock + permission to modify the public access settings. + Permissions: + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "s3:PutBucketPublicAccessBlock", + "Resource": "*" + } + ] + } + Args: + resource_id (str): The S3 bucket name. + region (str): AWS region where the S3 bucket exists. + Returns: + bool: True if the operation is successful (public access is blocked), + False otherwise. + """ + try: + regional_client = s3_client.regional_clients[region] + regional_client.put_public_access_block( + Bucket=resource_id, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": True, + "IgnorePublicAcls": True, + "BlockPublicPolicy": True, + "RestrictPublicBuckets": True, + }, + ) + except Exception as error: + logger.error( + f"{region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return False + else: + return True diff --git a/tests/providers/aws/services/s3/s3_bucket_public_access/s3_bucket_public_access_fixer_test.py b/tests/providers/aws/services/s3/s3_bucket_public_access/s3_bucket_public_access_fixer_test.py new file mode 100644 index 0000000000..39099ac39b --- /dev/null +++ b/tests/providers/aws/services/s3/s3_bucket_public_access/s3_bucket_public_access_fixer_test.py @@ -0,0 +1,169 @@ +from unittest import mock + +from boto3 import client +from moto import mock_aws + +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_US_EAST_1, + set_mocked_aws_provider, +) + + +class Test_s3_bucket_public_access_fixer: + @mock_aws + def test_no_buckets(self): + from prowler.providers.aws.services.s3.s3_service import S3 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_public_access.s3_bucket_public_access_fixer.s3_client", + new=S3(aws_provider), + ): + # Test Fixer + from prowler.providers.aws.services.s3.s3_bucket_public_access.s3_bucket_public_access_fixer import ( + fixer, + ) + + assert not fixer("bucket_test_us", AWS_REGION_US_EAST_1) + + @mock_aws + def test_bucket_public_ACL(self): + s3_client = client("s3", region_name=AWS_REGION_US_EAST_1) + bucket_name_us = "bucket_test_us" + s3_client.create_bucket(Bucket=bucket_name_us) + bucket_owner = s3_client.get_bucket_acl(Bucket=bucket_name_us)["Owner"] + s3_client.put_public_access_block( + Bucket=bucket_name_us, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + s3_client.put_bucket_acl( + Bucket=bucket_name_us, + AccessControlPolicy={ + "Grants": [ + { + "Grantee": { + "URI": "http://acs.amazonaws.com/groups/global/AllUsers", + "Type": "Group", + }, + "Permission": "READ", + }, + ], + "Owner": bucket_owner, + }, + ) + from prowler.providers.aws.services.s3.s3_service import S3 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_public_access.s3_bucket_public_access_fixer.s3_client", + new=S3(aws_provider), + ): + # Test Fixer + from prowler.providers.aws.services.s3.s3_bucket_public_access.s3_bucket_public_access_fixer import ( + fixer, + ) + + assert fixer(bucket_name_us, AWS_REGION_US_EAST_1) + + @mock_aws + def test_bucket_public_policy(self): + s3_client = client("s3", region_name=AWS_REGION_US_EAST_1) + bucket_name_us = "bucket_test_us" + s3_client.create_bucket(Bucket=bucket_name_us) + # Generate S3Control Client + s3control_client = client("s3control", region_name=AWS_REGION_US_EAST_1) + s3control_client.put_public_access_block( + AccountId=AWS_ACCOUNT_NUMBER, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + s3_client.put_public_access_block( + Bucket=bucket_name_us, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + public_write_policy = '{"Version": "2012-10-17","Id": "PutObjPolicy","Statement": [{"Sid": "PublicWritePolicy","Effect": "Allow","Principal": "*","Action": "s3:PutObject","Resource": "arn:aws:s3:::bucket_test_us/*"}]}' + s3_client.put_bucket_policy( + Bucket=bucket_name_us, + Policy=public_write_policy, + ) + from prowler.providers.aws.services.s3.s3_service import S3 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_public_access.s3_bucket_public_access_fixer.s3_client", + new=S3(aws_provider), + ): + # Test Fixer + from prowler.providers.aws.services.s3.s3_bucket_public_access.s3_bucket_public_access_fixer import ( + fixer, + ) + + assert fixer(bucket_name_us, AWS_REGION_US_EAST_1) + + @mock_aws + def test_bucket_public_due_to_policy_conditions_from_public_ip(self): + s3_client = client("s3", region_name=AWS_REGION_US_EAST_1) + bucket_name_us = "bucket_test_us" + s3_client.create_bucket(Bucket=bucket_name_us) + s3_client.put_public_access_block( + Bucket=bucket_name_us, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + public_write_policy = '{"Version": "2012-10-17","Id": "PutObjPolicy","Statement": [{"Sid": "PublicWritePolicy","Effect": "Allow","Principal": "*","Action": "s3:PutObject","Resource": "arn:aws:s3:::bucket_test_us/*","Condition": {"IpAddress": {"aws:SourceIp": "1.2.3.4"}}}]}' + s3_client.put_bucket_policy( + Bucket=bucket_name_us, + Policy=public_write_policy, + ) + from prowler.providers.aws.services.s3.s3_service import S3 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_public_access.s3_bucket_public_access_fixer.s3_client", + new=S3(aws_provider), + ): + # Test Fixer + from prowler.providers.aws.services.s3.s3_bucket_public_access.s3_bucket_public_access_fixer import ( + fixer, + ) + + assert fixer(bucket_name_us, AWS_REGION_US_EAST_1) From dc4e2f3c8528b74d71ca475f88dbbd7b051d1502 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Mart=C3=ADn?= Date: Thu, 12 Dec 2024 19:05:25 +0100 Subject: [PATCH 66/67] feat(GHA): build containers for API (#6032) Co-authored-by: Pepe Fagoaga --- .../api-build-lint-push-containers.yml | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 .github/workflows/api-build-lint-push-containers.yml diff --git a/.github/workflows/api-build-lint-push-containers.yml b/.github/workflows/api-build-lint-push-containers.yml new file mode 100644 index 0000000000..0951f22eca --- /dev/null +++ b/.github/workflows/api-build-lint-push-containers.yml @@ -0,0 +1,81 @@ +name: API - Build and Push containers + +on: + push: + branches: + - "master" + paths: + - "api/**" + - ".github/workflows/api-build-lint-push-containers.yml" + + # Uncomment the code below to test this action on PRs + # pull_request: + # branches: + # - "master" + # paths: + # - "api/**" + # - ".github/workflows/api-build-lint-push-containers.yml" + + release: + types: [published] + +env: + # Tags + LATEST_TAG: latest + RELEASE_TAG: ${{ github.event.release.tag_name }} + + WORKING_DIRECTORY: ./api + + # Container Registries + PROWLERCLOUD_DOCKERHUB_REPOSITORY: prowlercloud + PROWLERCLOUD_DOCKERHUB_IMAGE: prowler-api + +jobs: + # Build Prowler OSS container + container-build-push: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ${{ env.WORKING_DIRECTORY }} + + steps: + - name: Repository check + working-directory: /tmp + run: | + [[ ${{ github.repository }} != "prowler-cloud/prowler" ]] && echo "This action only runs for prowler-cloud/prowler"; exit 0 + + - name: Checkout + uses: actions/checkout@v4 + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push container image (latest) + # Comment the following line for testing + if: github.event_name == 'push' + uses: docker/build-push-action@v6 + with: + context: ${{ env.WORKING_DIRECTORY }} + # Set push: false for testing + push: true + tags: | + ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.LATEST_TAG }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Build and push container image (release) + if: github.event_name == 'release' + uses: docker/build-push-action@v6 + with: + context: ${{ env.WORKING_DIRECTORY }} + push: true + tags: | + ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.RELEASE_TAG }} + cache-from: type=gha + cache-to: type=gha,mode=max From 554491a6425b865b5f73dc13cbe3a560964415ed Mon Sep 17 00:00:00 2001 From: Pepe Fagoaga Date: Thu, 12 Dec 2024 19:10:44 +0100 Subject: [PATCH 67/67] chore(gha): build and push OSS UI (#6168) --- .../ui-build-lint-push-containers.yml | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 .github/workflows/ui-build-lint-push-containers.yml diff --git a/.github/workflows/ui-build-lint-push-containers.yml b/.github/workflows/ui-build-lint-push-containers.yml new file mode 100644 index 0000000000..41c0a11017 --- /dev/null +++ b/.github/workflows/ui-build-lint-push-containers.yml @@ -0,0 +1,81 @@ +name: UI - Build and Push containers + +on: + push: + branches: + - "master" + paths: + - "ui/**" + - ".github/workflows/ui-build-lint-push-containers.yml" + + # Uncomment the below code to test this action on PRs + # pull_request: + # branches: + # - "master" + # paths: + # - "ui/**" + # - ".github/workflows/ui-build-lint-push-containers.yml" + + release: + types: [published] + +env: + # Tags + LATEST_TAG: latest + RELEASE_TAG: ${{ github.event.release.tag_name }} + + WORKING_DIRECTORY: ./ui + + # Container Registries + PROWLERCLOUD_DOCKERHUB_REPOSITORY: prowlercloud + PROWLERCLOUD_DOCKERHUB_IMAGE: prowler-ui + +jobs: + # Build Prowler OSS container + container-build-push: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ${{ env.WORKING_DIRECTORY }} + + steps: + - name: Repository check + working-directory: /tmp + run: | + [[ ${{ github.repository }} != "prowler-cloud/prowler" ]] && echo "This action only runs for prowler-cloud/prowler"; exit 0 + + - name: Checkout + uses: actions/checkout@v4 + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push container image (latest) + # Comment the following line for testing + if: github.event_name == 'push' + uses: docker/build-push-action@v6 + with: + context: ${{ env.WORKING_DIRECTORY }} + # Set push: false for testing + push: true + tags: | + ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.LATEST_TAG }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Build and push container image (release) + if: github.event_name == 'release' + uses: docker/build-push-action@v6 + with: + context: ${{ env.WORKING_DIRECTORY }} + push: true + tags: | + ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.RELEASE_TAG }} + cache-from: type=gha + cache-to: type=gha,mode=max