From fe4938f0bc2d930acd28fd65d24c2bd5b9011455 Mon Sep 17 00:00:00 2001 From: Alex Gustafsson Date: Sat, 30 Nov 2024 12:28:10 +0100 Subject: [PATCH] Add support for vulnerability reports Implement generic support for vulnerability reports for images. Implement vulnerability reports from Docker Scout. --- api.yaml | 26 ++++ .../image-page-vulnerabilities.png | Bin 0 -> 60058 bytes internal/models/models.go | 23 ++-- internal/registry/docker/client.go | 65 ++++++++++ internal/registry/docker/client_test.go | 15 +++ internal/registry/docker/utils.go | 5 + internal/registry/oci/client.go | 2 + internal/registry/oci/models.go | 1 + internal/store/createTablesIfNotExist.sql | 10 ++ internal/store/store.go | 54 +++++++++ internal/worker/worker.go | 2 + internal/workflow/imageworkflow/data.go | 12 ++ .../getdockerhubvulnerabilities.go | 111 ++++++++++++++++++ internal/workflow/imageworkflow/workflow.go | 16 +++ web/api.ts | 9 ++ web/components/InfoTooltip.tsx | 2 +- .../icons/fluent-shield-error-16-filled.tsx | 18 +++ .../icons/fluent-shield-error-24-filled.tsx | 18 +++ web/pages/Dashboard.tsx | 11 +- web/pages/ImagePage.tsx | 95 ++++++++++++++- web/tags.ts | 5 + 21 files changed, 490 insertions(+), 10 deletions(-) create mode 100644 docs/screenshots/image-page-vulnerabilities.png create mode 100644 internal/workflow/imageworkflow/getdockerhubvulnerabilities.go create mode 100644 web/components/icons/fluent-shield-error-16-filled.tsx create mode 100644 web/components/icons/fluent-shield-error-24-filled.tsx diff --git a/api.yaml b/api.yaml index a6bc1fa..12ec811 100644 --- a/api.yaml +++ b/api.yaml @@ -207,6 +207,10 @@ components: type: array items: $ref: '#/components/schemas/ImageLink' + vulnerabilities: + type: array + items: + $ref: '#/components/schemas/ImageVulnerability' lastModified: type: string format: datetime @@ -257,6 +261,28 @@ components: - type - url + ImageVulnerability: + type: object + properties: + id: + type: int + severity: + type: string + example: high + authority: + type: string + example: Docker Scout + description: + type: string + example: CVE-2024-9476 + link: + type: string + format: url + required: + - id + - severity + - authority + Graph: type: object description: A graph explaining why the image is used. diff --git a/docs/screenshots/image-page-vulnerabilities.png b/docs/screenshots/image-page-vulnerabilities.png new file mode 100644 index 0000000000000000000000000000000000000000..5f28d4ca29640604796d4ade154d1b5e31c658e6 GIT binary patch literal 60058 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

hzBlXXUDh zJzw0I8@sz$TA1Qc=?T1nu%UegJ3gjeLY2*@AbHkUZmyf6oxL<``tA|3ss=t7+`CSC zvx1ZX#>MdQ1Vm_f*Wh81@8#!+QqI((hQ8x_i6+NAmVu>VNk04jIk3+^Z^e+l`SQ*d zi%Qwe*VDnaYyc6tjI}PL${5xjF3Fu4JitsO{w~_M5Mf z#?@8@GM|@c`)ch{ATWgLn|ecoR9I*tHc;aSh71}jjR$Bf6lOG*p%hKlXt0oAaB2{Z zglI!5@FK|g!vf7ZxepF__kQFk-`|1X4NkEm{uXlu@sQsRfSx%)wm<*Ehl&RdU{`(r zIudBYs|o^@EdxrWk>7A2K)v7m-JiyujW?IB2L5vB8L!rh!4RvmMko zNBKx`BnBwuWF(=J{~3fN^V&o!))+X_ zxs$vb5)0CL^{W|ESV*z!)1TD?aaxzHc)EHbw^Z^n?%K`1IV%LWzTiUnR-QoUMP(XB zD%ZN*YSbRQOMU;583$uMGEcdMWvPLVbdWuJd^^R@@OUdkVA6_x-o{%^8NqdeD-9Uy5<$*{3NIaKzTDU9$_&oei_d|w%J z65n)Q?<9V3uX*59c24ddf_0&?HAK_eDr@6}z&MWQ+2<#Liu#n*GH78@43ErPgiW^(_)5g4huEYThfVILzPn_q z1>K8G(S91j`gnH)#~al~k0Cm4YV@c}s=Pqd!vjf}IvY_ZltN71R) zV3L{OI;*N`M;;BLEdfte=t2o$)OdT0+PZ&J>_pS zATxTL4})XC^Xa{uhe`(v9%uUZP+(yvS$Ghb=8%72ft zWs$=T434P;jVAJfNfkk9Pv@)e`CVl|MQelu6iZKO&?(Z}Y>@I;B6`Q!>vCCn?))Rm z#La?#9{N!V!ejq(FjF`6MGVCdNk>W!mAPRVZ!)MyiNFL^FyjiE!JK4|6DR1De{qKy z-+oE(B-~0!>O;bBJJ7_&xW^A@TN@{rvs)U7-R^>5GUQHx{b*r%k7jD5j|!NeZ+yal zCq2z>;=${ntk2hM)k#wf-IwywD9ieOHHu3FmHAzug(-seahA}I$EXmHuATLJJuYTP zUZv>>E(sr33sP&Cf}21CckWm(qDzgLW>_*2QE;R}CX#;GS*(waP$*jZD4HwnWCIoq z-$(wIrudhOi&WM=YW5&D^yj{A>KJhyBD7AVG_D2)v;s+1P9KtI(o?>J1KNK9v|jO- zlY$=-lD_BO$Sm8LKl8R`9+*osd+~=sQdZU%91&V!#wBAFzs<^DI9V@52@RHH8?cXsqbIta6pJUvJk61KyG@;6LFLY9SN0v-~`JXnE0nfEuk{UKlq69=#p zzTn}&k7xfI8A}?guOoc+D2Mi3FfXprmy@=h+Z;kNY&FQ~ZOnrb|J*e{4XuSe^Ejpo z`$b)QUH-)J{C8CaF8X>hM`0QG3v#k!}ndRwa z8xM{{g!ZT}w*Br>CzY-QwE!(G`m@Hvsp(GB5d3bXEaVx7BMQXxO1~>##bhLk1WhLSH3F?~ zKI5)MGM48`UbiyFFt^uKZ+3UdbZwWp^5sBNb=J#JAMSGR0`~Vqpkm-JrwTj+%uero zEH|YbV&9Bs&^gXnX-YijlhTp~O6Z2qm8y+hkEGci&(BAau6z6;KuPp4JcRuZQzQAz zh5amTIE7G|Co+^26)(JrlMMFEv*!d!*M8OnOeh(g71{r}4%`bLx*HzQw@#dbu9R}< zn*ZhtNVg9q&g$YyC~M(7{})kCY0Z`tdZ!N&me-W>QPy7{T;Qv0rc z^!F(|Z}74aDiI<2(M$RR&BwHr?7DoAnrI`kHVd>ark(7Bv~QLLQOK8UPH$Qzgw3ohKZkg93H6KN~sPiI|#p@(~x$t@`v<)i~VD}-R6g{NjUz`T$M%$Nz z1IEros^lBdfBH5L`WjW!>6@4O5zdeL=b^!fyeKpu4v!h~WH3G9_w9w@NXnsl+oN{0 zVGKqr{N?$-4-@!w-^dMjFcUWW9ArUsE+a#F1er0s1T4{q%GvS#FBJs`YntjM2Ik)K zN5IU?Sv(1{Kjixy)bE?v|IxY59q==0vTlVQ5%ildIA{UvhR?t<)Ye1fHnP?>f>a_8 zq6_i=D=p6qU^`iZt94w4`WcS-{jY%0g+4BZ4i;3^T=9+wNj0#y==zfj9xXu-Ls)I} z%2khh7bN$CkF0hZXmHM%z&)6&u}OK!E|DLaTgr2we)ZGoCoJ{ks7%ujUCuwIg$;u&|L9L& z_Nn}?+#0%{?NpokI@JQaTp#|$V|>~pXOUMeVCanK#SYW+iT57-ye6cEF}Zlh&21UdNxf=4?3;6$UlZi5NVH z{s(_vIY~$mNYt(w7jpk8V*NzlSCD_z<^J7WN%QkzHPAQYa_(4+!Qm$jKhg0Ax~ zNR_we_4E!SzLq80FQ>P1ZhU7vwmEa5N{4yJZY68Mq+P-LF&*?SKO3Kd|G8KKkw?qj zNo`}-hVPBUhz0hk5I@<;iHUy$8pw=lIta+?2d=&~n(WoXIq(BS zMsLV`a3JJlSjeo(4uoF~|Bu%XU?0rA8U7$Au%Mq$sDlRLAj#Jr?%+=Gx6PC~6Af{I z6>B;Odf#7P7`rt6t!UV)uXDr2%QK3v62c7qgf&JWdFctWk^7m6vdyj$NycZQeohpY z+vp}rYQOQQ{iVxSO&-V?g7>~jF;pCWQlW>*a_2J8EHsMi)HL>o5e*;5=`y6A4C1MS z!`i#jY2a@v^JR@)3VsTYX*e;-38GKpU$joeBQ~xo&n4M8dgUBzvUu$>;wTZdKAQH{ zf6Tx!K3r+CQU0^oC1as%r=yK`7aH?gcGC|LCTgZeAFdgD`SKrzd%;(u^={^ByKn&m z#iXK-Z+ymnpFCctcwOJ}oBhXot#17!#rP-D@7`#do{LOn6|=qnr%t)gTw#q({8tV3 z6HmQTKWiG;o_6XtIKY}Q5`lrA^v`}{b`b)(CZEh`1)fG2hf^&31XPbj-}Z9Te!}qX zSX#L8QR>BFC9@GL|Mfc@>_?Z=v)uc+{6n70dpaea9EF<%Y6>9nE_lfKqA$|q|@%Xt@7;b?3V*V4)#i)+z_Gz#cTrt#bx3TCqW+7UXe{Gd1nMIbRj*K z)Cahd#S_#KO!I|74f+LorP(MUfLJqL zChCHR20r4UQOHnsM>v2tl(g&HGrA|{3K!)-#~jl7voSLBanO;Do36D+qq57p z0s9RhPF~c`C8`hB-UlTrQVhNGXiR%(_?MnCFhX}$|_tE+>(8LM_Of3cCr0bV+s zy1b9_*)@%TVz84lZMxCLc2u(AX_d4wU&up?{x|5Lv&@$=2I&dKr4KC4O2aPITe!L( z&kS|SsVsknf*0sI*@63LWW#B2^I9%FeIRYL@2#88QhU6XUZw1(TDPyv3f8#QX3#4* zMzQ~u24f@=Zb0gCO+Wmf1B0_2{JWHMJ7{nWErX|A!Yu=3pz{z}C`l7WQ05`opnRIj z0`-4$3o2Y-Gaia9)NuzdIG~=B0RIVaAiaVA<`&duz}Okc&TtHXaA6_DxdZ;^>Hr*o zRap2}_PGi$e}i?k|0EPLHL?RU02F+vKDZGf_ z5eVUOqxjx2xDhfX{*z_UOR)ZTH<=Hf!TZPqZN_iWl&)nr}T z0V*RwIN56tgN50GR-`q@=Xbh!!-+HP3lqPM+o#6zQL=6gMi7xbcd#Ujkh}zxSe5(3 zbCcfQu8L;HPYA0oTSwhC5B1fQeRwbqgByZZddbt$jm#elKrwGhuyT`I{}#A|+AM ziV1J_r~gT=sJr~s=c47!nLg?6=H)J2%Ij0vc5=xr({> z2F3T!H-ASNU4c8yV8m;ok&@@Z)%Kc%6tUudA^xe1$!|tu*L-(?`CL~csVv4fYB=ys z_ke2Sr+FINifB|QE0Hwze2aa<=?;h0cG#lVYoU`p5PhKb6cCfQZ{{hR?H?xS*B;mW z(z=M^OPDW86IniD=AoP)!=fXmkK`LQ^>Bkpg20rXS=Im~DaysrD0}qjmy1VVeI?;9BeJwUA*OLaj1|3!` z5$!@7lpj0%Y(Bd>i-E~meyJE~lj@DJA0mj*GVyjVrxW;6+eo#i>$)Ed9{jwb=qSGs zoy5XI-bDXJcufl}w(K}>KC!2EHCD=LjfWvVwR0@=r_~Vqr*2Y$(tMo#-KDskFZR zr7zYiYjvAN*Y>Ak^1RwTw-;xX(piu5Mwe{6KdpkUypu@_51ySIJ^an=lvB$7Pt@$6 z^?vqm)dgOtlY@hwALYowHyS%8FZZ`+Qyxx+H^`C!4n#CeGr%sGpe7Hpt>(vDYlA(i zjRXv?;MOmAcr5sR3eQVOhIE$MIb^{VfRHEf=?mgR@CTRBkrc6BG~}R!T!SIJfBI)y znS%s~5p066Z;6AbziU8tfZ3%AgD8W)gCYJ032PX>{PXYix9z{z-giQgH|H&ozbIF1VZ7RKt^(Ya)`RmVNS_*iU z0(Uk8^K@RFe%t;gX-n1cl-dG~(hJz_msSjT>y$g`C`>*BKE=$eK58qNN(5$swywG5 z-yhf|eo~Sac88&lrqL0mJr8i9ySd+W)Ci$w&XXuzndsg z8R5)a;U1s0To&pvb3Lm^^c3Y#%xp7Fw?vx8mguxYgx8Z8ujA`ky1vuC6}Bnebsf)w zqR+F~%`=$Rc55mrZpjtCZ(t%omgT-ojVmw#9al!0{vWp90xYX0=o>%xO-iS9iP9w< z5(Xe0(%ndR*KL6`(hbs`(kb0Ycb9ZY*LOgl_r3n#|9h_M!E+9~v$M0avpc)L8RnHC zuEeG-GyWaZ2FAjluQa<8z9fy6G6A|38knzq(&|os)IaVp7%r?pB6&J8W4chH7wl)lO};OD8!eI7x}07889DJjjX0#i~{S2EfCxd?=gJyq}82i_hlQq>pKa zy9DC3rNApuvzx~)cG*}hSKvvV!^TXZl0lhpO5-&@OSW8g)^H)lE7$tV%L6Dj06_%M!A?E=gYi~=UV+>=0H zN0pcgZ`ZSf|q_1obCI$FxwdZOb?W|w*?_=w?~C) z$8khFWyMp?x)EdP)|<zI+bCRX~zTy7Q7$7l}@zZaMFC*2_LTZy}Qc-`37o>zf zH|zv0drQNyxtW}UGqy?qJR7q`g;XmP?)58&JNBAics0cr5VWprj$fZD-K^;;+LYIm znDivv%oR=@vO?6}W|_*W>a=~qI!&N-a|_k&epniW3LSy zKxb1>^sP;p%}a%gIwqkO<)u?yj~!<^Ph$HpRh>E~cyvtn+fL+knIV4SN%pl%34avK zns68czhGS!y1SVQC16-~NOB1v+SV!0bHMI399`rN@1Pj&U|8+l3y*`Xl}s0MFNfl4 z)s(^FLsedWVfKT$DOA6W1&3li0O{Unc(32j{Oj2O7I@H@fd(7$fK(ye1RQZd#0t2B zd^qYr4s{B-|INQi5AO89qYNe9M_^shNP&kQ4~EF1;2-eClU@S~$_|Rv2l;DRO2YpE zU+Vt>UsmtU=0m04jK2?mO-vbxIl2G*_pRrCRzH*q7S(&ODXKJoPrAccpZzWPpY=Q9 z9IwHB;5e)a{x4hj|FAkWiXigvfr{mS4A%Dp|2VXN47;Vjx2O-j1C3nm|5FsROh9L} zEJ}mtx;N2U-E(Jj6!T##{gLotG6s^2w3X_$G=d5KWbKq8IDIbtt$jfYUj;Rf9 zPku=Nj?NDVGT450nITYRXxSfWn8)Fb4ckPGa;6gsg_}e2)dmfiLkPIQU+V!y}1)ECiic&Q;;Bv|@Q{ z2KRe(fTZ*=!kQ-Q>c+W?UH~iCab*NZ+AKdh++R{EK0+--lZ?~q0t4BodgSd_!q7G7 zOs4Wm5Z`HaZx#H|k?6B&)qrteLx01(sG(SR4 zu31QX~&6)>*|=-fp#wVesiabP0fZDXg@-M1ji!w26ygWP=QX{e65 zfcf}xuUw8fP!|F8J^&aaDGYP%(^d4zm#!7fsz&zx>!GsSjtv%Y5sbkGugJk|x6fVR zelG!H>h-}l;~Tmoe`BxX*MF}3#!t>JK-R%MYaI#fLmexINR7Tag%3T?Z7RW)>mTWR zP*%0&`ne#iSrb*{qPuKQFx+N;8@}FqpCzUCo-q|Ictjl{*a*taUhmm?A4qGltX2Ym z+6X`RR>s`o|6Hl^n19GDlTFYg{dfHu``?8&+q-I{`@^0N6wD z2xXc`_ccL1O-Sn;?GV z!3tEjtp9g?9n1AUoJ&Xk$9QxCDfEHhzjlh4_Wn7>|5{{7So!bXzT7vk8RQ0RQ@RF} z;e*RqZXEKjya~9Es@DgcFN>RYrUSn2P7W3wB?kW>yFwTDaJJ1B*j=8p|Krs8BSm1T zyC!DB*g~1cL_ef+Qp{$#-P!BBzb9VZ)E#%kZ60HC?iYvf_H-K#DA*QLvO8SlgLO#k z)T3~N_CK$E?u>g)9DlWKr7I`Tb-jV*?)8c#B$L0GiFH1FEf_dK`fh*nwpX!~bU{6; zC0Dwyg)~&;SgSpMrj;N#K$6Mrky>bBtW#EY_VMzkN^N-t{j4QAYEy4#QU!B25f*H) zZ%-*#VoT%uO=D*3axmB0h(g-4>y_E(Mhb;b*PSZj4Kw6T#M9 z(fAJ6y|b|n<$BTAV@{LajCj@+arg0$jEuM~z04OMru}5(tev53%jNLI{+aje?oM*J z6nG{F2Wr(h7o4ponZHt~PXfUCNe&x^8`!iS>)r`opr%k@HWBr=2$27r7Ii38NpfY4 zK63ij>W1sxKc`#WG`2!O@gs!z>*s;NH$?~=l)Kkt5eXO6?*0ILQ7s>+jw48mE8@a( zWl@+80&w8@Qbw7I{{{o2y=|pNm$7u8r7|vpLwx?ixbEzmx9*2yLkx=Sv^D(rns*rv z@pOflJVb&li39>PfB@!z@Qa{$0qFkDp5WxEW(Nkr>&qmqo}mTR%ec?mx&|_|!{lleF*UR4Nb;*WbTK^#~4`LsO~vEYnMMo34X z4ZhKfnkBtf=HCrWtVUupLjsQn+8Mj`UKhSf+nQV6u<rAp6{*jeI zt7P=rjGY!ZBGxb(Bqh*{8Ls zn~>vF$8U1eD@{)x(NyduX%(<+Ruv)0#LlHgf>TLMXqy+l`3J;x^6e z4i_%-KG)Roe8D|*H>;KE>n+c>C-ZD~RL=7JtQoiocR&4E!JKV2y2o@2)GUg$VvS4%R3^Mxr9vI?G{8w0l!Wd&Dq zNU7byW#)1^P>jw~QSO1zHC}^(Tc?beo!>T(HJ=o=`zv=q-tUQ*Q%l@o@MiWt{qqJ% zj{zTNYItTYapGH*f~^xU?lSDYI~wA>aiA5WU?0mdRaWC1e@I}pG1#U+)6^H!uQp0C zn-P<*%JeuR#`ewZg^s}dFRvQ4(ZB>+Ugwh__h3*&IsjtCF%oJetp6lXgaaF;J22Zy?_U<(C}fL0}r7w-LII+cbsm8i{B z-z{I9Eb&*zAy41^d@sk20477^-*&DmB1V&o-t1BGet}LPh#Y8GHmI!N18-w{w^tW@ z6ErNf<0`jwIu<{#Xr_=@&3C0zJNG_xou40$H*xkon-8k~-BxiE*!yeX4|hhs%@tiT zsg2JQK+~i1C*g_K>z!Da<^Eh{YU4wS>jP^9EuCTEdMsh!q|3>;$f+}N2yN?mx$uyh z6vpE}l-gwWKd<{8cP^B&Z%1_%*y!(qa^1-IennUr*Og_k^V3*NS*b6KR(8=m9Hm0p z4phltRmomVhF7MLYNL6+5x&vB{OhHIvu_0*9yshgJHaYMNQ=1p)-|7l?$lK0KDe;R zpiCI?5d~$9TFoRr{c~c)Rd3FxrRp14d9>V@kg%%MMe!5OI-tlU?44caaUn(dw!bp{ z6-mHFpp*xFwDx@Xf8$PKMdi5|mHl7O9m7L%VkQy9Q5Br!80s zJf~+mkdJzgNW8H*Kh`S)cNfJzcZVr9sFb>2^2<|tyz|;IiDzXe`A1n8VLRO;G`P)( z3-D{gZg5vbIi6oo&AoCqG6Atr?aHk+2DC7!t7w5ya9Ht#vMhWU`an=owUq`N`=ml znX^~zF^wAgjY`*QH=Xr6@C3OC{2yhJ=Noqw?T%0ry5?*T$tX?B5z%bv2FKP$n(3G6 z{+uvD<9@u~zK>G5v$^em5@1j*3KWXsG`Fp~v|aq-QBi^g>_7~vO=Sv*W)q)g65}u&troq z;n_l3X-{q6!m}B)9|Rc?3wvNv{;>a+gGA>%qE|&|ZaI*5z9+24gLm3Ko1RSxFfY64cD;+fd}50#4OB&J z->4RL3cNa76q8CoSyVou*|I!hDCpr#&D}lE<+%jn|8%7ui7uJ6$uSUHbLNHUsdbGM zRs2>1E*7O5`gWa43Wz*I<47#Lwh>uQsOGilRZqvi{xNc4fl1iE0*pVa0ivWrr5Hs0 zh7o;*>GSb5DmZs~JR%HymS zCr9CO{P9~2PkoYQLtpHy&%M&Hi5o5P{4JjoUm2Xm^`0|FsM4Xx$x2S?&oev`_?KAl z2nQ!nja;l zK%I$kMJ|&4GKghnCRmbq>U>%0nN4x?#jCuF8a8V0aMkNK=2Bl=#%P5|Q7I`Bb0-eU zBx_N(EH%}5a6v~gDutENMB&?U-T0r?{En~baF`aAqqS_T3o^r4!)y{`DouWH@lO0HWp`SQ2)mvpT$wE;gPz8)$#;Wjb8p32Q8GOjg^5bzK4 zkC2xB#=7`L|7gSRdP5PXueHzU$*tVF6ZwHOV&kAoLUCEtYEy)x?^3t_>-}!7?Puks z?m0f727aUj=bKAJK`>b<3$zby%}WSwqJ1RVOMu7o^%()SU8A8vF(LJ6@!ipR4*xC_ z@mlu$skM`sJb(@VG7OUuR+wN)EF}XI-A)pMljopm4!;Nv~T^beZN$RD9!AL#v*FNOb~m(p{jXyB#yM_^#%(1Z5H zA+$VOZO`CjJZICQZ&mluW9>R-KJ@wfEJJk&=qDcKEKrc1U=&2?ec%v?$AE9Oc}CPp zDwI_9f=BiA3Ad(J=^HRS@e+*Ams3dP=IU#;1%4m6COVkCcibfuh&?^29xjE&(_(Lp^lNIhQ7 zi{-^`Dqct2jjs$iBt5JUW83zP$jN*qQSNle26cT>M zFqFEL3}i@}C#6f>4^Ni|%mw@@hb5xRkv^X?jg@NUsKRj*B-yoA$>3;E97?OTqjMHV zfz_FbHl=AD^UX&mU3T?%V3!RZ9!&8&F7NaoHObMAbI>TM>3p}ZVpKs=pXqWbf6a9= z)4VWi`iajyHYucd0mH*k^8-zOp}jRe!L;Cy(T|Oa zCllb=WF!V&SS?q?SzP>_Bl>=)8EPTSzG!}A z8(GUt^BjAsiD6ts?vzPtmb?qU`7?9Q#VUrgr*R3FP;~gWC34W_^qN}rEKymI=vtRs z1Un}Lo9=V`wB{BcF_SxQwp%ZsW#A|_QX+q6g6y1TdNj-VtyTtZVbyo4U9!U+J=wXn zj$&WFvqFjhzJJ7C`#9i*G<&98!mxkWHR9zDiDh_|8X_ll4fxE`0i9BwM|$z$iEh(v z797Kq>&F?g<&3{P>#v@nP=Zsco~H?P*q?NTqNesYgbn|_zAXff?5qZL%U=)<7sk(Rvn zNN2QW%MY$Ld@1iU+n1y4UyA6N8SEzRRQUtd3f0}PkURNji8i?$GjRofq%ST~{^+05 zI9a5`{I-|$J?Ch{ZtdwE&y9UkX;EkgWpJlh&K2Kt$S4;aE9{ck_9~$0MqU95wG!Y4CJi=U{JRh)E{Sd+mpATU=2{zUhYGS`? z7TSC5@krX>6t&Fu^}hWG#heZESG19&Ef$XGT+L$cm*jSPXQCte=8G>*MUsR%23c~} z8;FG`L|%WanPN)pg*$E>Z|Ff_vkVeX?`Ri&p;F&9mhG<{Y0}$SQD(neMB*CdJHqOS zZ@Xh#YF#QYY*dmzs5v2=G$2jT8(35+wGkD>V30+mk>N6pc6#3v7s>X->x$v3sDwV|&o)MEIcs8n^1#=lTFIBIg4%nLWIaS`N zF<&lIG-D@r zh|!4y)ppzNwnwYTlWyEP1V*JOqO+$Z<&M=hZ*=^RWy9i6@{;Nd=-H|UmXp=oHJKw< zn_bj2XQdJz5SCFxL1FoPG8DPQ5ROSk?vJ8_FgR=C>J&)DCg02@N9b@zD1w@ z(rWv%KU1=PexI|zVBvA|OIaDIT73~+q;Fw~EQm6pP~qGz`$c}MDjVg;D2viAwVC{EtG*CkE$-V~6k6H~l9A{!5ILC;8ij*>_2mpQ#t={?5qdjVT*;bE)LpFzb^I|AJ0IsmxY8O_%qX zGTGVWLWMfK&{!jBKO^^|z_eM{%+{9Ptz|dcx)D{P=}K^S04hl)3^<5@(y=g3*@VxBqu8d$hXY5lVoaNDBfyWPyj_@McPe0_~dQ}yRvo^Q{ zeI@QV9HG|kjLMN9G75{2H3YcKjJm?>1UOyW37xB$Hffy%)J>%texmTk*=MWXmI0` zXizqCYHK4vU97|%DBoyF;;|<_W3uk0&@da>^iNiK_b7EB7NdL5O%ZQBD=o6I&WDo4 z`1w?T75PI)kBU}ez&qIwfSAC0UHTLa2IpWpwz_0{~-u(e%V$3nv| z#B}o?DscAZMDQ>sUX-q=)o6B&E&q^dnZ1OID3T_$)y{n%?w(J#`PE+zRd1rn-x*u} zM5gdREI?IDX24vv>9-!{RbQ29NB8lT?20USHhOld&E$l%w*aP1n#xu33sj1eyuXT1 zhX4;t!E5Gt_IO^^N!os0(q>l)TKzqT+?tb`e#ZIq;~?JFauo5^VMqrqmo~lgKwne< z@iWN=4T774aMud@`o!rMrHg29SOqaMpRt8$3DarheYF~EiGES&|DB)x*Kq|P8}akC zDzimGUPHa)Tg1k^l^oW~3mqos+OoHRj7ys=|J1ZbpW02C2KqK9_V{tnCq$ba4=2j2 zu}FT>8k;W+8sFxETXFLyW_?}rAH%7T3b@p5i6n|mZTmX>q;@kJu1I=!G2f+;%OhG# zO(Vn%tI%ScPE5h2)EIb7dUwnmqzD)5Z9B9!YOlPBS&g_ z@A>AzK~$Lk%t17J!bWLUB&f%RKcb_N1H~w7GI8`qQQooI!`?gI;%Tb`E)|VmoQZ=j zs-DGRKFZqD5_F%NG9BgK!^p57WpU|d0u-|~-}YQ-Z}&bbtJ8rhvcHy!*Q(HSVCuXsTQ2<)W5o|KuCot4N5(T&!*|^z7~Oxg2t;dgyJI%ecV}}Y)jZheBvWQW;ju(uev1fTVkgj08qm@nXZmWa zMPHXq-9YKG+7n;MGbnp7&PnSA8#H|DZH;siz4;hl?AzLbAYO~K^qm&W9;Tanb(L8! z9TPRQGeLS$#F1_wUOamptOKVu(&W58-ecX!D`~XL{@zRh-8h&>D{j!$*?0)gc9=|I zv`M7&Os-;^U)E}$SJ_S^unvv#EbmrL0@MV4<0r+Z1e#0Qg@mUgc%{J}Kd z>HtJM!42Q5p$`}rKP^-A4)u;_ajgtwFh71+C&FCF=z((tC|&J_$K6rHicx?oEBUQN zg|)VXOaVOPJ%x6Ze~-ZZmwRS4 z)$Xs?z_eokaPS*}t^Eo|FZnGM45)Jb$p-^m;14i$Vu9l}3Xl^rFm^T;zjH zhwGzDK@9^q3c#vW@V0Bc^R5TTk|$9`EoSPdng=pEtNQ9_@^DZch{d@YSeb%CbG;#RyC};oFPBDw{h9Y$4ia;r8Nq zWdu|1Ko}-auZKdoCbH* z8@Os8_%M1s*cCn&unycZ!&U177xn=7A0_P0cz+)B+a3cU9%1-6wmz91x<8}P8VR40 zYR%*ZDf3pdAqv}KzcobbP{zaZ#X{4$t`1Ym9kwD*iov@;p|6||wcF?mO6DjnBXH2? zYNFWD82+uM^TGKxF^&TY=(&$+Ark|sT7BSnMzR3RPzcy6Y?Eu7ROG&W@Y(DkCHE$H z_X$(9!xsTy;kkV9Xn;KTIyjt?nR`s&r!qS*C3mXPdIJIgtPTHp4A$%GgT8CN>rMU2 z0et7Zi|?PAVK@kN_L#Ah1Xp3MJL?3FV+ZVmP;MS!-22QatE1XTSOJ$d<3+extp(7} zPcWdb8}CeZxb!vp=tR2AP(-@CnDW-|^y1fn{2~{K;T9CDgBGyKAGrNw#*bU#Zh?Yz z^awdf8n~@93w<>5^j7{#m?UVx>6`|O@JgPuX=0a6KYp}H0QJ5Yn$J_~CO#n4-QQ3d=LHr!Nf4&GyVJo_}3Zk3qPPx{(ig{>wO*h z+Iu()vIyNoYyV;Mj8`cCa?T+07lINgrTw?y+COX_`|h^qUo47z<6kTa9sb7O zBAJRV$PdUqx~c!+&4lRr^pykLZph?6O#r5IsxCgF>u$!`CS zoC|06;VFSA3HJ)PChy*iAIrM_6cw-qp(v@OP+#3g&Br4>VS93+$IgsV9VwUuFp{M) z;!fv95cl-5-vd?89%$5+lH55u*fdv!CD{m14OwcFQ@p`^3|nXoocF4_0|7*s`J2oz ze+cga`q+eQ)~U|Uml;Okxv@TRkeudWs-Oo5{R6J=;10CkH{R$h&3*Qe@^QfY14fjV zyZ#Dk-~sto8vs6o%-GJll;FA0Bx0U(HkSUV15@q@IpZu`3*Ne6-FYSoIhNMdc-LNB zUZ?>ByPa}Vdv|3?=Q_LMSB^L0)h=Uu&fKS!BcB>w>WY_N=GWEfrq5QE+AV~fa`Pwf z5KOh{*IUtFx(*BN&Rd@7ZXO@an{ON)^h)o47<&1jws0F!Llq6QhjZf_bnaw#FC;0< zJ9$!!bI*mQR(6b0#fT@;A=S9DRHRH#)Pwd_S~Ws>@hE(@3OO*E7^Zg%`P4)0XT&;0 zCmQg3JZxGkchAQYghNMS_WLQlHwCk=ivN5l1snR#xdpdfwaABb5Fu^}Okx%P?+9#jYB|HLpaJS%qKbRF1C zpZ6I(pafM1G#ssMz%@1e*sq(K5}Nb52FN+5bYp{tkQq|ALzq`mV!$7%KCfR zkkIwFAa$Pxr!pKG!j&Qc0puzcOpItyR!eq1UnY28Oig2cn;d|$?bXkHYBg%NXC@8B z7~HJ)bjC|ACl(GP0W&U0rqcuZ_JD7QyI^D5O1~GRdMqUumBYsG26$o7e&hT&7(tJb zPk}W^-E&kxvVK?-ewbyRn~=b3iy7YykK8j7VR7V_m&+8X{?SqP*tu9mgi!cjx-1WL z-KWXD^?1Z9$EyR4YWYgUx@m{Ni>`Aa|=GEoE?;EQ~AArw_&nVfbfV1I8O&@GUPS?D^ByU=D z>z-z2n^wfI!4Qw*kLi%uqE_@y17Wl*QdVC$wk9jKJ$GB!rCY6rn@7p}nxsYp*8$_p zF3;;TEj%xg9DMorA_VzyimC1M|sf=dUd?cEkcr2KENf(gvWBsa(X8 z32EnvsxIAL2ts&qlMfo$0LJ|Cb_j1m!R%Y76`uiLXo=noC9n`=ZApNzIPoX;M8x}^ zL~!lvW;Q;+biy6?y+F77Vo94JwIs-9K#l%j0_^RH0c?#%|xu?OmsBK=8kbsGk;`gy~4aYn2r1$wjG<`?Lv1X)_o z!%mYjrd6pui_b_@Ra3g--!LBr($+t=;Sq!|*+EN=oGBebWQ zIW_O@&n_Hq>0AQ(Y)FC{um!J&Z*fxXi)2A+b6dHFoD~ z%M1cBtTnO6RG)6NO+O8~Jyy%$gg zq&hkRkS_r~?Spd}LE=ZvtNAZv^F<+qH;v&&^V11cL$3FBiipSI$G5)M&Q;!J$WQwq z*0dGV=Z?xRzz#>!fg7z4x~R=`m#iLVSHoh-NVcBBp5{{+Znm=#QtyM2hb!pT$z#Kq z+5xU5)eXtA0k8xV%l0u3 z=vm&Q+JK4sckpq?kbLi7gjS+2xD&nyxHaq7C;kP>HLGv&{te&+K;rksHD@2HgY7=7 zR?Xh)pljD00y$R&bn&2Alh_f>&9Y!3n!j%^$-pXvFaUH=PVq7S)#UvzOAfDD!HV#| z;L-sw!tMcN7t$gKdteck?h00Wh+8A&9=AgQ833Ei)`oz|gTV)|H){70SYTiN(FJ)w zW#z&A__t0q^uC)Q;z<{YQWrD{BH*uqzh5Xqoo4@~srlQB|2jwD`40?ftMwpLu#IuG zPI{0ESUIu1ACK+817WfEcj2zEr7nimRa|~{r+4ADTUXb8)a(TCL68V`#<^EXFpzJv zGDn}(H=AQzO=q}Ui(%=(TkJc5$Q;aJ4~96&yWR^O1sfLsLq`x|)#EDQ#_fMWsBN#%%ccLi;P_53eCRE?)hqrnF%afxbjWF zk~pw)EG69dMj=CVd+k%Q2^-Qq(v^Y?R-VV7fDhY^n0RB(X^fzE)=YK5zjIuGv?yqI zX1;KVXZt=?do|v79IoUd3@xG4J;Si6N3nS$+GH2o#fu*}3GO`sQ>#-a%(-aj_TG%_ zqJhp4@(vCNwRJ<%YeQ=uF94+Hj#wo$%iDH6fWR!pJgIF+EDp!f*2o#)j_Dmc=C=*u z)wAAK9W0r*w6Vc_e zCv-aOKxmLpe0=dd#;BpVFG~1iQ`qe#}q^@{cKMy zfPAvsg^dh$d-de(M!y)%!X>nlE#zL02tmi%4RKLL1voWZ%u<8IDjqv^H$4LqPAZi@ zNG+T9Vl)%Dk1+Mf_rv*Hbk(-SCPVkIHcPB|ueoQ(*Xss(!C+qzs!?p1h(7edAN*rp zfam7K@toljGsIP=b3j6A#=YpG4DOd^aj#(ZjiEIMwbw~62paT-8_Lnu4ZQhGw5{;T zW!dYm)WiM2p5el9&?(B`S4Jg%4h2FwjoWNdr38f4s-N zN3;pVYE6W!&e~a64Z+N}P~a5Ixj^#q33`WCNN8R#S7+r+Rlei$Xxj5ex3lzh@=Rgf zsj@?d75gHlL{k_z5hzniLp*>Fb4YsGLp9m}jhMJ!kW_m5$jq+CYS6{X*js0S1}Da# zV`L{-TxX5HO&$U6#X~#NK)fswYE$WcQ*v>GHAeHxZBym$Y#A`@1)K!H`6sxN{7E|U zL<=?(Z~DNS8LcMQG6P$>SbSKhY8^vfo5$K4^#Xmvgn^&oTR(@s{k*6EJTK>7i2=#% z{GUD@D;qzmIll>BJ|Zf|kyGPAzm@kYP#AwKTj0L-)@f6Z@AW+sDM4V$<2uY{)0Ga_ zmKnXa4idv>f$WNIX{Gt+K^6`GdG5KJr%rNo5N)0$YerQ~e2!p5Y#ayi4nSJG*H;gn z)T>7!=E);AT{rlo9b8JHYWzdVsI-9Lxl%lv&+d}{(8kzZ*gM=iDQS*2a5%$24Ev-N z;GGC1$5&?~&MfUiYgFxUQ~|~{Cg4d}zPjEWa~hIZptuSGB)$9`*{}v&vH zLLpxQBytcm@iA0bV0zOJqBM7oALaMpq_vIc>0>24epmyv;NVZ+04hm#Q}*zwRqkmHZy0(xDyNW}FF5YBx)Pz41qga$ ztm8*#Q7VAD6pn?5yA<(5K(vsbU){YvEplv6Yx4u07KL9nd-$B6ZoG4d8q}B#t)jwDh+|F!)X3iM!~PW3Dm23 z8oAr${NNA=LP&3gJPjKtZ_1fev4@-l$RTTY3EBc{&R6=poKL`*H-(XK8&4-qZc2>nd5 zj(WmMm}nGmKcV1MgrJMn5IX*i;EEdnj{?O??3>)vI;4`Ncsu=)5=xVlNdT|HmsK4I zu>H>L<0=sgp)=nnG$wF5lyOC$>HE>s6CNNOyA0Z+E)jGJi0uxbgidfb;@CrW)p_Tg zVziQ=%Ot3#f6LZzHj|b9NxdWPzASJM40n)l;Kmcz1HSQcgW(0Yq0ocVF7|g1hM|o9 zAreEPYt7?+0|VL;OE8r?s4}2pD}yQaAFL2%-Ri^q1=aHO8Rf&S=YwVZ_BnWF`cP%( zznzl$m;a~~g!leSG8ks;@L!U_FfhIIe~AFfz(fF2|KF{WY%yu$|83dck-=Voj`f4} z=-LBb3_3naWZ3`{U+WkBze6;B{1>7@35L`??88Ky`Pj6GK;S7I$gpT*Kn*rTN^Ph2 z!Aq0u$$n+od+AtbDeEn3SWM*>b8oH}KZ5~cq&&bEO>dku?H=nnqrnI5Z*&1ipRyeI zAv@PV)U*~wieu2e7K@)I5wgkRvHs}vHdn`N550(K9C>Ja%1I!heTM2zYed9oZgM5; zR*Y%@!!5BvdFaEykE`vv{1=!fvHMBdhOhegjei|KC5DBXY!j#C-OZASAuSSwdR%$^ znk(RzvpahN4$o%p5;ee%+DuNbE%=BB!tzy@Y_DUObSWl3ulL%CoPO@rPIR=hTgD8- zi{bs86!+361hrZ(k6RYf#bUHJaZFW=ywY2r;}hkm@~dbumPvfI=9?fcvjv>jk{g#a%&@^J>-H6O&Hc+KN0<{K6^aP6D(yQWFLmk(oyT`3BR)eTBSo`=? z+asAOadC~237Vg9JsS0LP`}!NEJS$Y2FpRxb)vKiyj11 z#o2T@{s06w2cAuAkneNd?Ou_!#{Y+LGEvYW#_wzz{#_UC?3xs@6W5_vLNHnERc!0kae|?wE_TROCJC zG;Sc_VWrmqloktic{r#0$BEYqRZkiS>Av)95+IXHgPimI{{35hN>g|*?7MS?#3=^C zS1?;Nz4YXunGkI(fyBKkwedvDa^wk1wl0RS> zh1#OkbAv|!OB-V~&XIfrCy&NiYqV+Lc;nib_(eG%=OfChJ?()hMhO- z*&(AD=%a35mFNV<&wS(vV}n*WlM zc2zRM{>$>A4hE=Y{`KD;%q!S%wkn41e_4(Y?p;dIug=8~A%{X_X#iS_LL!QHb)KX7 z7W&)Iv+6z2yQ?%?Z|LQCraF2sEoiw=DQ+lE5Gn!Cag=!TQpq`#_b|8{ZC;N*#QUAp zwttG$+E6R=ge<8Lc-jCgDk14*${~Cld^?Wk<*UDXcl}QH2(Fh#5p#>NlBhDSr#Shj zp5hBKML_r=G(rD`^;^4IKOUBt7$W0#lDO+-DPI%0L+6Rs(oEww-tln~yoX?RyNk+; z8~AJbF!QD#Wc9h#iEW1AJNWjU!Pb0Q^RUKaqqcvAF)uZ<|)<2P=fw+3S zB9b|D4ivj(0eCKPG3v-;bx2tF@L+zYQmTCk0RjoDkoPrer6c3EQ#pdmJtnvDl)xhq z0L({-1*34Zj})Ho0z+XNj@_#Z%cfVPJ^Y}*%qLXKnA~{4h7iktT?S=JLl3m`REJTU zU;B@JA`&Zv5z3c4fESjpRXy7gFy|0OL9_e`VCz-$eBsyUC19rZf$?3Ak5a8)@LaBn(+joXt^ukp&K_E%=*|(Rp zLLu8#ifR_gn*Ls#EI=V<{i(iF8q$UeH#WwqD!iuz9NTFdQt#NowoBS}IYR4Wi=`z- zn0ZrA^YBS$%qVTmoxpPF=)Cji^>~fX=l$h%i#`4x?}o}ZS!*1v#8{upEyVhGTCfk> zM-dc@vv?q~h_jIB%wSWmOK6cxoZl%I2(5>|1z6@T_Xqb2mVNhnTFA(X)7uEZ#KGX- z*M{fy2CV0J_ChthIu358^)=Ok2s|tVj;%Ef;N*Wl>qDwc8tV7Nch)2@p}Ezzn?eIK zpxjLg)J?o8t*td;fVM(cKML8T{>ZOis;D{}tiOeg2Ki3(FJ5LPB#Y48CZo=&Dv zyyFzQNp|SrR?tl1ZoPSqUooeyz-^gzU88&TXSLdOdOPf=eayPqouhe&lAU#d+JvEa z`?7bCZyKTm!Sgg**73wlRIR*lrL9H|Bukks&Yk9t(BQ(-%rlGKuQjnK z1Q6k>Jk-nFk><5*E}uLuE<~mj4h`-4;MRqBEc95@KDplWcCcYT4#b4)*k};W-3a{1&dq0RI53$bAZd{oDYgm1vBEdnyX88LsnyW;M?7ChofOlA;$#9oi8hqU&yD_QQ~H{eqRdM@ z%E=gDq>*f(?d`wG70SFN2k{H#Q|>wb_2g#rBQVc~P>*#^$@SjG1I4mUy(7!+ry2+p znV6SN4gC_G*d%mGaCdoksX2YMnh=#?#3|;m^X3UsZ8S$suln3)-4>@#!*N>8jYphL z9v9vh@0v>qDS|mvY7De648K3-;;>}}@enRYbr`RK=0O@*TEVErFHz4f{aplzrRB(1 zr&TED>!I!WwCdyA%*ntWhI&!Am;jBoh4V#SLE?xyo)#^aqgjDidA=x*tXM{F^*1`~ zlrUp@mDU;{n)B09ZB_LkcduD=zFY5&dBjr(>ImbXS(QJujXjfpO@3e>WMJBODV6s! z%Njf3Fr9{^cTx*7%0c=)Qe`{pv6l@54N(^9Fl?ixRmUJJ_A5MMtL?% z$XOIQQO`aCP+I~w7`-0g%?-i(cx2n@g%+TReGJd0l4W>d6!|DW; zDC4>k)KH(#PKnxq=|(cjMs^G+aI>qKx#4z2xrU&7_}q8RH~v6&i-xHdO*!Dk>0NW$ zM2EFJ0LP5L>Vs9Rd!2MjUP!a{=H1S7Z)&kU5!4t%%LP_p_9|zMbpndq%Kj7k3vV~* z@A=7D3}ej5KL@-L;xg3)AOvO+uqRR0mq><5L0@tgzxYQ+))@&SLdMKcG;;N87mO-Ri^Y zDZiB_MW2}=Q!{*Sqiq@V9fJYWaqkqMoJFRijq5E2cH*@!0jrt-)+#1gxdqYd-u%bM8H$B%F_dFM>eB+~_^m|Vuzz#K=ANIAxWgSO6T zP6n|jNGJDtD*h9MDN!)PqP^7=JRt>Een=6%>F8R(cBUsa?dh%JX%Zjbc(rrTkB*E{ zhoCO^*mC+A9zT;ya26^l)S0d*vIs^`E@!Ru{JuU|LPgjP_{HZD7QzDpNh^b^)%j@RoF)Jh z$^S#vTZdH@Y!9IO&<)Zd0#Zs#ONW4TcPic8bx7_VM2P zd++<+Kf7jT&6>4W&CHsa52GJ|TmNg;R;-8ng$T=4|I0#fpAe_a_%94&i)E8fJe!q- zI#Bf}<&)BGiTVpuuFNXqzS;^fH?!feFS8p1W7k&kKJOgH8EKA7CdP55F%i&Ul=L5H z;EFsZ%6Iq*Ii~zQQ^Zbdk-?3+lgqmdBLBXP-;6z}uM0c`qn^}Vu;`M^u}p^{A;4z8EUb7uD4UEpM^bvTFg`R$Bo?4r)YwCth|w^s08zKj3f_Ok1`V)#zAPtUJGr#8 zGSGu%YNMl|fQndA!g_W`KAM+Ai%QeG(N`9royg$%Q!_LIVfS*q;ezYBFDvlz`p%Nj z01=duH+kF|vlQXE�_sDfrn6KXDz8ChNImRi&qC7$_C9FIU`r%#L_nkc6$v$KAdB zQoNNK3j9>fGyw-Ftsr#OB#*PYfr%SbCpz+3Uuc;vDTo3Oj(z`98}u11pA+c;RI+QO$2njXCnbRQMYN$z%#f#{rM# z0~72BmBFjnzobfCJ5V9~H_tgCSUUs>4B3M*_|L6_qdEWo1nD52|0}C%2M$)lBLG3T zyoYe32ag#9-_x`p-JqcTx4L9tg9`b-FaN0(j>S*oXKUi)|3w1|x)5M)usGD06$(GD z`aggGI5apu!Vl$LaF8p!i?Zd(rCQ}R)9yEs0T~CH9G&fmoLi0= z4Yqwmgcz@Qj9o7=9S0UfY6Pllo?!TN`K9M8W15K4^H&ZT#ru4BQjASpu31};t5%+K zzORc`(~Din^e8maZw(K|G1kiSrBlO7IiFEn=#jQ50x83DWxM&G$Z!00My zg>lu#UFJz~EG`VN3>x@l9OG`aWJ6RMS_vXdvrFHJE{28xv?1r`RVY{wgMpvmYC;4+ zB1gc>9`J&`Mg}CtF)z@a0UCHc7skID2X#vV&Pvmh)!`Vtokt1*Q2|bs_f%Bl&v8N0 zP$S)2k1yC`O5C-BnrB_&heTfCN)!@rmiZF+`JFVo?mR zP+5sGAcm%g3p9jYX1=??NqY?M(iWVtalcAjs#MkZ(!m8(Hm~gupNKwkw_TEz5E*v{ zMt*ueUFl$b$n+q3FsYDjP=*anvLjDDbj_5H|9cnErG^E8UN51D522xnkIMmHcwDw8 z(?#!bOD%m%X_^{-hA@-}-C+T>1*+k5Q~_K-vDPv#fWr_zD|Fo?xkqL6grfObja{a7@*(M{sib_xjUVMn4E@zY!1cSnqahp zNRHb8-|l-G{VqFMDW`k@;K3RA@&gG-2{9Y4i(0)b+#Hpiq>TRMz>&T;r7jAT{#;!( z)@y53Y~`&DKf1^~i2V`}*&|Uge#IU)TFvL#)QC8^*{WgA1F&hX<3gW+yn**&*64r( z39ptMP`5+2Pb0BxHFAW_8ft2?((rD%@n6 z;Mx9STR*ONz*}flvM3?YOGWc}yrkyxhVp}h6>=VbbG6IL$mg4tT{rYLrD@)j8V&bh z_Nl3qM0U0i|1^^W8Sk>QaK2_1xo4S*zmM3b4)V)0qhwmOS{Zy976a#4>YDRhyeP~T z-_3phJtSUaOIRC6wQcGd3_+|(fB9QDpOUQg*3Gmjt*{hIvs`WQZs&{t>J@7b3DBe* zLZbk^mu#!6I2^Ns-In(CNIT!JAVNu4yrna#ORYLx^8{lC^q%W8-hMx0m()t!AofJK zd2d8njv%H?6aaj$Ic{$V2>!MBEq{hL2{;WGR;Q9IpaRgu9s3Z%SWVFMh7gkPw%X=$ z+ztF#2TX(R4qfvYX7{l~8NE8&{F`2exd;3<%#E=q|4aW)^P68<{P*8xGzhrPobhP6 zR>o4ZrX4TCqbS-+5nkszP!5#%hfA+6Z8WRUY_G*!$0u3)IG7eVH0L}s(mx6$WZcG| z8RreFEeJopf0Gy&w?m)AbU{g~T+l!L6nfm3?;v+bwqz9`<+Q6W2LMKD#;<=~w#+t0 z5ASl|3Lh9*R3bsO@2+k-s*a(?TJddPMLad|iqYQ+FtKj&yQnGZD*r^l(|AL%8gRV%%4Py9To7AY&i)+c0~&U$tk zDri_3QCz+%{aiUddxd50H|LgYDuG;1F}d7NWaB=8S6#8?%JkS^Fco|4VqX!v>9ZE; zk~%IS3Pczs)7vsXWi@?`mbC7yGd0&_da>_vN_mRfMs4rAB|sYVCZzLDN>QC>xWwh| zm@$bEp$`FKtuWpM5Q+~42B^MCb@LQ3cOStvj2E;RXW~4F*@bU4u5j zMa6SMAmNKv$+jX&BG4#H>()phbPtloNAfcb9yeTRE)`TUO=M(*AY^k^D`>w&S2>es z3M!QjF^EjaA=0;y-yVt=R-z`5eHUBi9~+Yv@AVz`xO4vmNrJZufigvqx;8f^_{X;6 z;ShEMHYaeHmN`6kxX^YlR~J0oAvnhg;gCn){%ob~MB$~!YH|Gf_+0&qrZ{6J1|mvt z!b=H{TH=&Hyx6Z_c!13(OO*_e|9PM-#{sB)w&C{aJEremu^E(L;RK>eeRWZR71f$y zzRn3=7v*`Z1vh#&O2EoYPM@uODnp6!0MSH@Cj9#IU>8!km6bq3226ColD`{;0)Qv5eK=X^KyQx5vH6PZmsH z;H%}3|4H_m!$g9moq4eUE_5!jy6n#8RS1wZ+9v?}E>nWs!YKa8na&T+Qf0vs{Qaw` z#2{9(Sri$)nf=RW2TE*5W#g76LMdVDoQ0#1QwZ#YEGYV?YT>%N92&5X&^AcCE+xwK zd{D=N4-%XubJ=WzxW*PKwk-7WEp~Bu1!L3;_VnKe@&KyE1UQUtJOPgE&nf`nazM<-VMxqR35=!ZLxX#$fCL_rI3OmUB&jL3b~7gi zJPp$~Gn4I32YS@qp9_A{iuUlU|FMUB2oYVUcwhFDrSOEO4 zKe!d+038B{>Z8%A>K1w33QtTF(PYb8n|>Xhr-0<*+S@eey7XR)CW#Z67uU=WN8fV@ z>>CG;L!|=2YVk*i6P?nZjCnT7iLchzroXoC0hLBAowGbkhn*;P9N^;9mjDI;MdJwu zbYre|4-k`b?oPr=4}d#a8Fe_80F9MBu!Yxes2Udpyx^PW-)Y@RceRk#S!Xi zkWQiq9$kGMvmMmWd4x4`y{34E1*LfH!1x32@>-J;4a%M13V|{X0dMb4f&i-~=qHHD z9xlRKh$xVl{!BpxuFd~Bm5@bi%yD!;%-ZZmyySpqC%qZ5tvD9Dr=VNCN$d`h8uwQs z(Y{4Z{@YZCekhbL2GfJv=1-$eb8lL&mm~qLE2cQ=+{7Uvmabi&>PYu@R&3dRB#15E!-4soDYxa|` zC&Su}|#Tj9$8k3!wkyyti^=N)J*L)el}- zCuXg5@wf+^hRZ$$J9?10p7{Cy-1ZwEp_zdXiP=DxHf33wLGsPyQd$}8=BT@0(&!DM}Hg-v&bhAFMDU=@~3;h z=4vN%eKu;F^_z)DT2iUR0%J|LMb}mCwSq*XtWKH+iMNB`ytZ^ndOP5w?UYfy^kxhN zBRe2KLQ}Gy84_=JIX&oxh+YVM*82H!2Qa&Pe(=Et!2J1fbACmvf`jV<{mTyB6X=t= z2RR2ngK~}?MrgvO&mwehMO@bx`P4c3RYnBbIJgmtCJfv!k#+hA{5g+Y z4itG6du62nidmHby@z{z%u{I$T%ZMX56O*vOipANV7f)p}=RsgUMgTXKjwN;t1FEGNXiLj+&7=)r?Q= z`%`IEMzCE6QYU&0(BEujs4#{#`QVHVjc7|sN-l_s0f^N)1;Bow92R#m?(^MCa9vss z(*YU;Hi^K`VlztaXhBj~!xvoV-Wv7hIHEQ@rS5>!ItlPFcZtA#lac*BRGrxxi*h06 zsDxv1Pd0~wvZ%gJ1}9D4y$MOBQOS6iB_mGPU&g~=B%YD^6er7Q*!=4a!VBU~#Zs=q z=(Yy&_MHPF3vYH_AF7{i-z;hZOcc%;x0Q5P*AGb<7{Pt$$aH;aobcWT`8qviUanE* z+ol>TzQGeFyG81C{n~t%hD+NZb&#K4IUfL$K_sr^Gikeb#Rd^fVOwJLlb@|6>w^3< zvYGmR?V?nD*UrRS;<`}WuZa!1wveGBlkGB9T1i|Ud+z+Km~`VPz#(OBAbbap;`g*_ z6FriXw4DY~|9cSKM>W|C)V2Wd5EliFDI~1QT7W!{mgP9Lpg;?C&lznEa*&N&Ly=Z@ zYO`6(L*Xiwr;yN7sSmV&Wv-3dYa>bMOw5!^)sDNrV=9eBQ-b)hYVT&;4ptU)$CsxF zhX7jow#QKyWQwrgD-{j6^opnnZ_KkQW24^mth>bkD61bgbrytG{&YWc)*gUlVcjEa zz8=u&)+w|1Tf9%kfy5^iNY@pOY5qX4674`OUe|7`EzhiVg;soBdy=x=U8)aIVKy1X zSVy=}wue`-`F?UJkXcrVRw{r3QB7?{Y_q@Wt$Wqj6@O8#3-o&gyg=F zPlhaf#Z6KHM;sB>J1^FBU7L4v?g|Jqc-1DQ`8or2UCZcdo7F97uAoeTr{cv|`#CAGWFLlgYv+6m1+igKfAh6ZS(A%mtZ!RN8Cb9`8P7Sg* zA*^3S{`jj)|J#6&wx9vgm(>qVDKQT(PhMppyf7Tmn;dpcpoZAh+Sv_3PLSAZ{IwRn zlw2{h6N$Ra)m*rE9PVjOa|er6xSQ$`5O(8wP6%PM2gSJNumc$YBJn2P2nzI|nV7Au z#N$VtA)Drao1;jn&PKJnC?eKaxQNdR8-^{a#V4roTZ}ZP9mHYG*PZHjGecuj(A5E2 zTei#Z$YCSFA4<4#A+MbsC0UGtB_nR~j~1rvZ#-!4-X{XmOzI?sFj!T9*t3xz|)3t1TW7~-BZ2kNB6=~g<6xU zz3ej!V{F?%mK7E`?&ptl(E!4yP*NJ;g%Xg|Z5X#xMx;+!>`P7&SB&w>R=KTu5VBt3 z27rBecr3u*y`YJ~C^T^REq`1Z4H(n|esy#01-CS)&rPn(xOY8=mi^ieEPG1{t|tc2 zOoEtTMluc6MYB#ACi+e2CKPI+075(<3_u2r0NRAR;i`&o+ji;Bm(VKC5(K+Q8h7|` z-46nUYwEax#=DvJ8TH%Mpbbaf1nU#~_UXr4!Uv<{YfycJSA!7=`_JlPJf=l(tPZpk;E}5&s{iibC|+kF6h1r&Hsu}D zfet!&G(3F4^Z)xb=Yt3se*sDy8Xi+FHv|#P2-ep>C?N5_VkMAo=%oKCCcywd;Z{cu z5Cx!rF#c8XLKk?%08WU&#l!d-Tu-=RUqPCHa8wZgyT18U1e5xs7b4e@&830VMSSBYck2Y=wh_(=Mu-j|=*U(X zey6UWQ;|XmyN=Av+NSyVLMK`TiVU=I&n^Ye@hiM=Z8)k1@A}_y0`@ ze+~ucG1ugxKmi$w)9|%Oz*w8u-rk;=Wan!78gyIRHV{|uddGzPT?;p+{sV-GM&#oq z`w$;n4+;tdNhcElLChK@(6rq`}Rm1GKBKH+rj}zPDp5f|<3EtR;lm zV>rjl#r18DzeRKRr4|1azNcL=d^Fx^FgbEU=|4xnnR!~mO8VlCV#)WRH7X)u&9D0} zE~DtPa%!+qL(FiVGs#Cj9bhyzy3sGZznh3|5mfdUv|_Y-eAgFc+Cx%U!T=T6i3esK ze(bm0ZLu?sW*y9C?NaeDwqF;_Dme+e8fDwGFE4xBoT#2?vTW>J1=M~?GvJw+s1=ZO zvw7HQZ@fv={9F3bxlYOUR%O-8C0rw%R zk3N4hl=bW7Cz@`5N1neGWZP!7Wp!1IDx|cFnGi5Ns(fA;*}F4cw4Zu~v-vi9_owx> znYQG|dGmH6vdwM#+lrP;$MpeGp0anQaYQ7xq0xYz=d8a`Ql@=fM^JTUy=-(u|AT^o z@A-wodSmigrNazm(vkCokG|*3oz$w)l5_ksk&5!z6Gw1Xl%jyKU0v1*;%d{E-bSIzJbz70fpnRO2|2iR_ zh{{?Gvx3d@(O_MfhX%d6rhdmF)M63MtYd6Cb-$AqCJ-w2U zp!ceLdinnE66N;OwxWHiV2M;x7{$#guj}!Ck!EMzlb179hb12Osp*`WHJn8SWljur z`C9cEPPI#|3LzTH(ns}0nXzn=Jl;#lA`D-;T+S6L1ci|ilNIHH&Aul0+`VO=%Jy~6 z@29zJLc|Y}mUH>=#-CL)12OaU9ZLea+^9LYSSERt-K5VHpdC`Nf>{tn)yPiiDs(Mq z-n@`*-LPj6k5rOWES^c8igSHO(>Il$y69#hs1lw}CXO-I6hyAyhqt|)AF{C@!EESJJ&Ndy>@$?l`i1R!UmkbT1$1-im!kk=3-p7-;h<$9m z)4Ni|vPmpxzO?ZNi^Vhx31i-2W{1J`4BAE~u2;cs9KXXeqcAH#^?UFIj{_u6C4wC0 z7zX((%? z2V8mNrj&vwnwbv)+m5htcJ%KK@uXp#KVL9A8R>cYaIy()ZAv_?{}scvwD|H>zoh3! z#d23G*r)E%-Ysk;W#^KqAF+|fM7{I1HS`;PTLx80lt;de$=CtL?$$%eMM2g1w%`==g+|U=(@9S`$ zWW&i9h=+J>ndJ*i6>{+UN6fx`rYK}?)8vMp3wg?*$aC`il&Pvd#fc1tBlb!ZDmtO& zHBECFAAeY<@8shUH9I1_5mc>PuO-c`n0IFx6SH~jv|*z@wGwI7l-TQsO;7&IbaHZ) zheYErlYqH2#0tn6PaE$^c`t(M^!`3|or$b~&+aKsf$k2}a#VtHwaB8F6&Kgz>l`p! z`u1}l#gO)Z4$TfJOgJq^VYr*}l|UEYbU;4zkC}l^|0zSh^=qlkn&2t!SN-N7-Pjb^ z-<*B=$9^8mZ}PgzF-#RueWXGniui(UYI)sVUs3pBQ%7K#IxGVsr4~naEHFq-R&idAe7=8rYbGpn*INM2S0nV7D!FuDs z!{v_X-(R94C{B!+%cgfbp2r!2q>U$Du2?1xBh!=`TAd@KT2to0UTT@-omOfli~g#+mcU^#+~OS$CI*e;q{E^h~d2Bi<#=foj#82(`T<__xQR@h4r;` zhD1`mtgN(`>b8mHdV&tXl2(7oh^KzY3P+z{-}7|ACQ?C376D>Wh9VPgTjG{ zf`rEtNmX0)E-$Zo+1NZjWB%D zzWIk*{oxeExc-IXojpXdWy-n^lXU_puu7qHH=$QXF^cx`=?t>Z&CU2SW7RHuI!K^o zY-rK&EV0y*W94hAZktmtUHL?ta~Usl@rL-&Yf+C$>+H`B4pA ziTAH@{7nt}4A=~6ieC$&e?DjdCFe{*GtI+>njF^9U2COA#%paVxPENy{EkM_@nd9S z6(6>5p`ndqI!ao46XNUe^H<&ub(6kBzvXR)hP@JBTM4)17ZdGSePDRj~#rFrBYivObN?EYCI=veUcNL$K;3hIU zgf$|6k;s9($q}i7VEJGVC*I(o0Q+rM)R}sfyvm%G-Uq+qb#*?&oFd%1FO0 zzW~w%P(C!WQ6%?e^o4o6t#*0ljAB?pcoJ!0gp*wHZj&E5&!lM*q_!)_t;Kz^NZ{A4 zQd6`o{RmlHQoxEj2UKNm+z_@r^VDE`Cg&ne@Z)W(ynm!Z8E(kWPvO-YPn||mrhf%$ zKJFp#lv!n)@2+g*(@n`}Eyx1c;Yy;!(OJ)WH_OwGFiNTn=+;dEUztV2B+`uZW=bE$ zKf=1oy}_YBj0ZY?6|Zv&jF?%I{KV#My04M7Y9@CD)!C0i6E||6^8#fLP5#aG-46

KF!?yuzrCfLN;o5n)nW-^IK^|uG!Be?M}^IibOWnD<;`|~8P0R>T-yffjeD6*`l6gBT{k`ih@$o9ZnQ| zaj`4)yJs=!2~h_>AcoEJo(0Us<$szWpRjfJ)bhAq^t-TwCU$0&Nm%VjQr|LxytT-# zrF=+Rb^leq&n)GroZ>{Bd9xz(%mH_)v*EyaFc~j6eOCLk#pn0Uu8+~gNaV7GbZikS zsiySBbOLSNG=00UwbCcApaKKcUBui&sO{)CHb!X&6-I0+?P9mR?Oca)*)vrWnI=9* zR<2(@_v@PEw;0Z(+RZF`x6P)2K2Ke3Ui@Ahk(~ej{s1Vw%H%eC9?LxXyY2QLOM>DA zWs6+w5k`}L1YK^uTUf`lj7vzc-vKQx$@Uw!4Qus5dyBO#1jkAs<9;dbt%sQ>k0viw zJ)Zc8Q;xPAgZ5_%m>63+oI?>bZ7n@{^N`qt@^r?;*#6oeIj^G%shb(hn`)(pCNTc9 zwJ!|dJX!-G6w#>bT)AcOi?CgfcD-C!_KKKwEYff%$N*4 z4!IZzSHfQl@%r#)QoH^Az*SS^V@dwbGZprv@eMEg(MkEO^8hy7JCYC-ykMV3aL;DK z{a0AjATq3&*wh&Gq;LRWk_6Zcf8BpZn93dq9Mf-H-6TE^b$GiW{SbOGp9?2>ozWUG z2Rt{`h8UJIhd;bk<&azzE8vWWEo>b#O$0<|AN`VN1vv~ad9Pasht1xPhlM zv3PlVqEA8yKCExmygwcKz;1O5H++wn{?TYE-}|_r%=!K~qq4skneHhrzrMfM_;G&n zL;%OJY=Y_wxWL1ON@od!D%1k=a(IEHZ*LkFxaOwAKYc9pBt*8g+(BP7QS!lX-i{0< zz$8D`p%lpkEwjy-_%BKURXlR%g*WkV+QS{u_1%Aefu{vKrZm#-Us5SL z9WHoH<-2p9+F#w{e5^ZY*6$KByJXYQuGiY_(9s7qLU^wF5t9eE18qZMLy0b?bj1c1 z%=yNJ=77Y@0e{$-yUexvC*Z5r+k>&cIm z!swqvx>{z}c1`Xlse~eu1Q%}q^!~QN3ES;=5ffiM8>}E>RYtV^zJoiMQS&g!{rCK| zwHOvWR%}A~;Ms300=Iw^cq@=FiPoP0PIOMz$IALL+th-%gtEep`#SF}lBUsMMMN(@ z`36ZZWx)TPcj%cxm1iOG#iXesmrumYJxOIpA{(9C1~&qR@768ZZUa)L_D^a@6D_TM z>Iy;>jX6#!%^16)Og@xHp!&&k*S<7h&QqLp$b4dr3NpOaU!0#Mqjg0v zDVL(mur`@$;Sl-K-}K!As`)c+$`~S-tl`{CYHiQ8n*Ky9S8n^hT4g7tsqO4K(zjxR zb?g04Pxf5IK5v8Pu3AVRZXIBrSA0p#58`uU1eR`o%b{+Cj}^lR0;Pz&)&#+aRZZk) z*HKJfC|TMa5h0SZBYV$?u58U;#hl3-koZg4ob!1?dAaR$BaqQ{A9w)RYnVlfw zNqgs4(IFJ2uYW$kDK;(Zv;OU{3Ar9WOEZYEtp!8q%)4Cp%usVJc&ubi*Ut#q_LL+g z*FgBWL)9^@Hq|q9Jzk-#=mg$SxUOi0_;>#S<(HiosxHcncu8Kzq*9G#;wSi%A;P4X z5-U0kV$k2>l*x*CvM^L2-u_(PGN?2rk_@(^fP_Qe>>8(ly*s)@OXeT|nO{~F8=F+k zOSwkAvC7!x7?Pn*Z~bHv$}$zBwZ!Fg_qGnBG_6w`G^plzPaCo11^O&wo2*WL z2{?;kh}j`+enb=>BEvvO9IU3{?DH+>M<|GrrhEJ@5OST6?OqVH>3RMH7jZYzxVWs# zQ#*)JWUP*)tpsCsZAtUg@cIXa&?yPu=8#|GS!frqSnKD-oAtt;0t3{kmkggmVK1iT zvDf^8Ht9d)s-Y0NsQffYOqh5RBZ|{SWG(RqwU31Wmuq0ztCwjz7ONYdzb5XfT2|Ya zN4anKEp{8b#eL2lmlOz-+g3X)JSR1MfBfCGF-N}L(S~itV@!2eW5gZ^b)mJq{N~V~ zzIrVedhn%~v1=&aDqCNS-n0&Mlw+MS1|%}Zb`(C^gDWV1WYGK-H|({R|MYY!7dLqQ z%~}K~T{LIN1$1$-DvGMl7?t$W-c9BgnD%n@`HYrU7CRvJe|0?k2I+kwe()BtZ?4+4 zLZ7Df%pP$!=%gG$*)P#n*-x9Ow4~Y%epo zqSl_yg5O9e#QINeZOcBUg4Bx0jb6bZ8SI!5x%8pL6OR6asB4q!^D+4+L;bhpGlB`k zo^SqJ+mHCF?7%bTiwOBe6+pVh4(qy^k4<5c?(vJ_o=0&qS?7Cc-9j%>j)U)0Ni9+_!J8eChs385THk^;aeE8{N-{s3!Wyu% z;au?J?8v}(qovpcTcqT}&KXF64R$p)M5`%`7rr8@i^PP{Sa44Gd>aNgyG;jp*{)h{&oS%#@SYp~wiuf`Y7PxU0JY74PZNZm`t3f+F ze!8?;P&11RIVpsi{+(FX`g-w$cE~rX{3bZQN!N96iNPq3o;=$l51MpdMSC!rv|Oh) zifZ`!r#d1j<17gS>l2qIy-(e{YHR2HiPjw|>>4hb?B5oQIrMsCXu3$Nd$|~WK+fN0 zXUG}A4e3X|ugxa1Ho5K_kg4D-1@ zav&Yuw96l5yMBBV#4@3?Wh>g5Ov=to{`||;iFc`uCH7Y!{+w$MbJayeCL1C@A2z-M z0gB(tNs5DK&*c(XZu16$%j0C-i%__{wYi69gnwJNw_GL}@V5|UJouy-!O>4}ZsmGNvl3;s9Q^CLAph^6;CuHu$kggt(Newd*sJA#%7kjq&(MwzCi&(H^T~ z*B3hX=NDU}+C^BD!YSn%1BJyGSdIp*apCk!BK$)nd%}iOpVR4TjIY0IG^5h}`cD}gEX2);fgu_vJMUU&&?VsU^!0-;dVstt45YL zMw5R0jLxXIEtgmYwXDo%TGvuWf_v>)&2y86#CUp{-=4~n3-#;SbyF}3k+iJf1nbjo zOU<>&lj09N*B+oSv=qZAKpI(sl2O`0g5p#`$mE_O2(|O#rDC>6C4^H+Q3rHAFC`Eg zkLU2_E1WKq+9z~zeY47TZj{z3YaMpz+j3PTQN(^&_5*GF-uaa-)6=M9smw5Ph%>@d$ljlcqo0Iar% zcT$p$hSqtYCu^kXg$qh27xo%^wsJf2gMCoI2khC9!ga>Af61y;XJ~L=wb4lbG3DKny|*?ChP4$CF3DKS-B<%0_-b91i|4T6;BIK2(h4JRaQ7 zDi08J`p@zlkc;`DA;Fz_ zWzo`HDR^fd>^XoRv4>H9JZ=#jL*R})@B6YYUl1%ebnFL0@<)eGikOCRmjBDl4b9@; zW%z-xO^`|tM+59&AYT8$G7)t?s3wd(W%f?Ven%?0v4Dot{P(zpPfC!Xc9Tjr0`3*u zH+P1LseGrh0X%CElk!i084chcX;)WYGHwRAM<0znJl1&fmW2y>Kizp0JQV**(}!i_ zXGNtSJdeQ!p+rJgMa30id8dhod+bG^ot%C}cxvJiW=6e(U78f;jZCne_#;@%XjsdA z$zEn^-(-GZ^&tRfO3!fFaCI_AM8iF85IvCQS65JO+uFDHf~#qd9V<6otD1}N*mjCp z6-$Ek*jd3Dw8{~1Ld{DO;upDE@a9dY20)QA_eVSTs613RZppDk6LnNq{Aj)QKEGNH zSk=|LRnt$^!mhK>182odBKic}AOHoXNuf*`Qd5txETL5R>o@A(`kd8`@F*F7R^wkD z-_*uy#l1}>Oh_1t%?^`!--&w^{=W2elZpHFe%bgYX4xO>woK;F*7)Ry*=_vJOeJMq zXvXJ+-Dq{yxmvE`Kz_e9H-4e0V1d=(6k92J-%`Rr9J zIXP!wfFPoN9AvtaCRDZ2MdN)$&~(T&`m|^x@5+3Emw7GT(by2dC_~FW#d$GUCB%BB zpgc}XAAwhiIaH799|yJibsK^UU|68h9=Gd)FK^uB=s2*$@pV%?ggEKnlq!yyrM=BILz`&(z!qi}cymgVEC< zoQ0(NkAkhoJ!h~KrmD{*>3l6=&V5y;qiD1u6olh)YP{~_n?V? zr&m`LVjU{C_%|15`>KWhUsg@)z~=zcFsSLL>N~CTv(=wl5Y(~1NyPEpksCj}bCQ74V-avF(4V+G19QAY+YI8FGk z&Ae4@rIpNHhBBr6FBae#3380IxpKVjVw|Ec*1Xn^=n{S<68*lGCGwl2EuPi~y#B#j zy&2n_NubXY#3|r7p=eZOd)_%$Vvu< z-kl}SdiT~iPC6>=$C3Gb)UBnan8Xy%fZg>6Wi|7oeVo-JyE2#E{EK&v&3yRUIF%Hl z$cJ+Q6sO$XHXk`Qd4AIP!we0r=ta%{Z za)SSp?zEdvY=DV6gk|Am?~s|5N%daAtCW-ebEA##AHQIYvIT*tah|N-atZxpe%tCQ zPsl0d1t1r_U4G>!gSR6uk=ZTerrVw#FF)lfBt!c4X4|9SO-(8$0>4W?ml;V1slO@P z?XK5v4Cig3<&HYKstCDR1Z>JZ-Hv|O&iaue{CmDjstll9p`>n@<4EE~SFN7<+}je2 zY*js;>Cd(BFaR(XpfLo9!lf^s4QF^ZmNTQC@ly5G=4ZaA3gP6kGEwYy#XLpAZ*fMO za?RP|+_hS=JbO;6-!Q(8p#MXc_Et{!p!Ao}tUjaa*Q2#KvwP@J>Uy-eTdtPv@{l$+ zYmgC9KdGjyKqrl`E2gA@;jk9f(I@Y<+v}Z#{4j=7-MHP518#aQI>JT$ z5bv?p_G@ka@9(<1>#aNuEwoAZAhz-Tr!nt3D51cQAb?H$?!c_{T-1ZZn)X6RrD#Y@<8 za19wOU;VwH-gw4Q7Q%EsG8pJkpQH?4siE?bR>nzgflJt@QvVRPy+H$mCJ?Op|KMgB zjfPPkK1+G=AF}~Yiq9*6D1gNt3`{8>@X_Fp@XD1T|GvT>F~CjO;Nce_Ul(l9CPE>g z#Q`k`%*>Rs8GwHUiwyH3hS8hAOM)%pNC7rPk^c(`e23=;Q|#F9u4%0Pn+JyVuN(*$ zT1G4eZd~-=8vX$U83RTf?G7F~{9o|@qDbI5fPWXzZ0W-!so`~juuTLYNdJ2*ufj0| zU=(O;qsO5qW)e{zxEIS#mA+;L7ptO&$H^!999g%~AC+zW_Ccgq$nJCyP)uAQKSiI{ zxYRoCJ8)R~`S0_XfsV<*Rxj!JnI`YMvd|o z7IQzBO-8R<+HZe>kvXTwuzwnAEw%{xwEtg<%>PpO#Ylk49H8x2#-qrpL~PAX8x@9E z7r5f*WQ6pQ?!^enWouy6^g(Y39LzObY9%4*6bJ7YTlv%f*WOBWUP?8HB9bjEZG&{4D-UXOL>iz+Qdoc76PN|mGdatLI=Uw4tuWzbFjDNy zKC%6}^TXNVpq&ZIJS73{LSngw18#W_h8o@c34GjSk3dNFGJCv0`YLhQWADv-#e?$3 zS2hFRy*V7KfSq?3OOdT&?f@sX)mZut zrYA&)73AY6XY{IsH+oiZ&&bu=&sxoZc$M`xx_nOI5l58h(mTA>J(DX}0`nrZH%}sJ zx!Y!sh#0~M=Iqp1w%-Rs4BNw$_6fZ+xg%K9Tym*YMW$B1(PL)ollEQkSX$3qPcf8G zy<>j<2XfNV{l@2PK`t)LWqIX3@4+{BcKTY)J06)M?JqbeJGuDQqsQ4#prrHJkHdm* zYzf5|EJdmb-QQomjr_Q=yKY4@@R73`_QGTf+|F%{A%+I;BME+QFV`zwX*@@>Lrssp zUbCqO3~Yuh`aiS!(O>mEB&W;ISG@|3&n|CP^+)cVjivM$wS%?JXvM*0BF6`w`(v(V*fSgdLa3dXQu&EWi&RVbshC6{m+JWa zB#uA+0kL`1C#3g3SY*&3%at+>lfkTN{a!A@5~vUJp?a}w zqrE)7c&6HM{cu)n3>#ImJye(RLfGy`fkiKtz#C=Y?XzEB@#qE&xpZIj6Vj3PIAa_- zUu{;A%4=&e4|ksgDG9bOtUkPQZQ&F*57~9S9IbhyF1Lrbf-^+_>Z_ znw_d8Z6hNhT`{o3UD3Q8hXr1)cjH+3SZsM^CbaxadqxwRLH#s+Mu^%8iT+9r$2MEJ zsy!cK;RrjCYvRQ8asuq78S{N!4?%r-z0|M z1?vf{Kj*-xqXr%_fHlX0L&%qO31cm73i+HrJG7?nxZRQC%qFmlm{xt)B{__-7lBHg z7zMXAY_jTg{|su>;0*oB=OwKlnx%iP7hBukO5q_XC*d60!7`Pg?3kUB(I0T8_!w>c zL~4{U(6QZr&e4w5)yfe=q(t9@s5KyuIP87r3WH#^F5?Tj0kFdPIYy071gxy~(d8rS_Z7@1}i-9nrbJSvjd^Q8aL zyh}@xEcP{P#W!@Q+|>&fUp92?wMBIh}LbR-g+J*i-SWOcdH$`i zi8ffA&tNVtK9=>EM_|4GoxamR-w~>*u%f8WjIlQgs-QK7FA5;d-LQ{w&IS>4Z_oJI zAhqv+eW$g$Z-|DWzZd$#bD);af!KgU_C9Qs*Raq*58M3Bij{X+8ZbXH(dsYgG-{Un z4g2l>UTA0@qLTqa=JssqKZfFLl>n6(D1r&b_ebqhN)W~#`B)Y4bP37SoO4&}Bhsy$ z*L+`BGU`z<>U>(&;+W$A(gIrd3Y;`_HRldNeDu)Q_9ZD5Y}UcIiXXaS-SqsX=cfu- z^@v3#s{T$E$BBJt*0Pj}icOWU+=3B&f=FJ&@-hb%uTZ8=#8b>gsj@5s2?5K+_Kl62 zLz4IRr1SsSC+bQ$dRiS@wI7d8&Ffu(f8N5}M?$;?l>JuzMej_zVZQ2dCi7r%VN?RZ zYXtw!5CO`0b3{i{Cu0Lrr9PhCzp2i9F*~rPNmy@fa{$-p%{RG{BIL4=>gRolP+GS| zw=sF2&Gf%^CG`2pflR=tX9NIF@6y1>HG6>ZJ@RDAOZ;}pfU$-wP1Ip5NpFUip+r)$ zdboN)V&5pxu16-S_V3Y7qD}AtOuN_Kg9IDxpuWYhBe3QOW+9}XE-fXU{(E$t#*)H@?04xXe0h20oDRKwJJFP(! zAW`xqh6!p4zk&VB`nJ{qv{1QT&oVV#N z5rmR)BtY=mh)|ya=;R&ZK_d{8`G2Z9>$oVscaIO9vNT9Y_(ee)q*J6pKvHTcX{2+h zl@KIEN%Q7KIPO3FB)`^JknfMsfU!?+hA8!r_^u0LYw=hp&_<=k+BSzgR*)rpLTSv|)$ zV|g^5*DI3$ObCZ^|91PaBJefcex}|!{^0@>HZg5TAZqHqpZ#&+j}mt>YJ*LYs@tAsb!{y*`LgbJLHm%9brZ+}(i zY!STmsnH^E)HTTb48^Y(vckqPs*vn&J6L1*p*`yz%k{{!?rNRxKmO=ie;iM(y(b}b zTl@zDPNI^tR=+OE&sB>Lz<0*@+z|yHCkA7wY%1pqGaH8uihpEh>yv+;1u7Mr+e7;+ z`vY@c&0Pj1V=QuW8^TINPI)AUvFig0KlyRlN$Y&URQ22w1~{I0^A(8-u?kI%x5cm0 z{FcnWRxe-LaLf^dr73^t1i)DUUBV7A6Fl@RdGJQeo6Y$r%hGR_jBoL2@Qkkz~W zCk3g1EX_^blY3ykdeE>-=VNy62@`^jp^5K5O1lfMJ)GXMT_v^EI6wU`uq+w+&W2vV zG4{>t*B+yA4SYwMm-=K;Z=NPeSp7h`VLf-MM1iNH8dQOytL8*Tz@*MIQbuN$tu$)!DaTSyHo%|#unlfQujWto9D<2|KNt&4oSy<6vqtO<{m^)emx5=kIDEdPY#q5(HAG~pM_@ta}d zz~7($+yv2D7iCZ+*%rnV$g9|X_<~LeB6;@#<{o+NWrr2fBU|h@F~rX-)>&HR7*I&o_iv`E!XWzjN8a=fxifgp;0x z-IlToPfwbs#`kkT*6k{PY)mtyvt2iIcxzG+v%ciPJWcOC*;t< zMJoe7YIx)m4Q@CPzs)gBcj_{UaZ7oc(s%0YKml0^*C$MT7s*|3CjhrK01ET-UBUK) zpacZEMiMgp%9%6VVCvDftZorA6fxV)!1+3zEQS}Yiaii1HQyRmQLgAAJujnzO@C@)2h8OLW<9QC=?Z_JZz`9&& z{JO%ff=Rs^g2Ck$Cs2G))^W%Hqhr5mK_^Tv7pZ@%cB(;$_2dZVj_$lB{bREqp%%}x z$CJNTq{NVD>Uyy zn~ATc)}(f;S7S_np&;g5d-Hcaxo}t&t(4s@4Cg|i4R&pmI~1bTJ$t``YuEVeH?{Kb zTV}KO?BceBzrTGx4&P_k8nH?yp_d!GGJ&p2&VH@ZmuXBq@OumATq z&myah$|tE{vcuc~>iZJjw572EvdBSxe|^mm{x_AHme8%pCLrz!&fB;LilhA256Zwq z#*Z3|`R>q{d+m6#z0>)LYR;qunOk-=WBU-$HC!G>}ZFr8g3vHt-H=38M^TDG~5~Yv%ww-s*8F8YM;ekU0yI z<;*NyniFaVKO_-fIx00{zg)Dzb**Wvx-9Ut~P&QTrF#)uVA!P8W7e#*xvR(yh{A0+bR(^JWs@R z$SEk8=O{2j{Sg=4i$xc20cbA=_iERIT9*lP%Ky_mfFl!W0z<O}3ireqkgeDNOdD)C0i zb9kR!NXOsV@#qsF6MXr`xwO@f|5}p;5vltAC2+GxKyN&5)ac=!9g z_@pUO^ywGL9d!_sS*nImsI>n~+b-pmWc~2p6Qvy{Pe)|+uF9Pz?bPTBDE|HM=&Hw? zsb=E-Q}*1Cu=QG-J{WoKJca!+Y8VEWKMxPiWYb>H1}vmWVd#+d7}T1%sHf8OciMC7 z|JDdnz3>tPMHN3DK%Dc_(hinf&UZIznqn++a4||gbx8TXw_eK9>k0}w=8mF8K((1X zJ7uFFCJ?nTm$W5UQk(%bV3Dg(+N6#c;w++1sGPQLQ(x2k(%4VzMG<8oSjeSJ@z=>^ zfh~Tqss++=e%P>#Xg6hcE$ZtZOWXB|TppPYP1o0c^N&uJq8-h~y-4l1+$3iCv&Wnn z&bM}MeVX$f|IiR0rdoFYAWB$wtHcUA>B-!h<+I(AtOl;>$N8AT56>{xW;Juo^xLKK z|8!+=(J*XZZ-?cTTKdqGq`9t4sHZe+k;~-^P$s;u=Q#yNp4?XS_&IEkabn?7;^ZVS zaAN=j1#UzE{FGUd00B~T(w6WxZp^dlmC;D&y;Eo{Ffv|X^zCFxxzj06khqYzClb}Y z9+Wt&U;^|Tz4~mXdlsGF=HqWbH^U#p&#b_{sc}F%%(R++R^9o{(rtz3B3Bx#(!9w_ zPF4K0Z>`|=7N*+pP|A*t+IMuQ%Qd2gHWoNJ306O~(AY z=~ITNXlEX_?YA+BvSELh<+DlrB-QsI6Lk6)$wIqcQK%@qM~wy!0r@_ zCDuVYBrgS39={`TVPSoeAQ*FkD@C43`SxRJcXUsrHExQEPzxn4&+u|ZlmK~A-ph(l z9eq7-Ln2Cir$eHZY8@7+tQc26GUy5aQC{$p_h)`7O(DM zi`w?Do#%|CSGfKGUOI{m_yIVMM7e85e;_`$?=WXXu2ms->n)2!p-Q-vfetMAgdbSo z?}NXRH-sLu&A`%;fg~h~=%7k!I6M_kFT1+JuIJ0C_2&QB6W0Q8-`wg*{cLoc6FnH@ z_6vfA7GbhlY9x<0CP;A90bGSNFu5z4A|S|&Lp&c|wUVv)FDn9Pjf)*Vin~WdF;R(< z(Ob)!2VY$Ta*Q|Pz&44f0U2HR_mF%s@R!y2xorYddDRdQEh;p+ z>nBUl5s0fN8+j_;Xi*hY2qx6mKD?ty0cNBjQG6k^NNB<_s!j@~P*)~mzvhAe#Ygv9 zUtJz3_|q%MWmdW&wnQJ@6ob_y<$&oHx`Ozr{MYG)UXIh#jY94Ul_#^w=O)!{RtBy! z3q;X%Pp|F00?e8py4RPtCl-6%H#WbRJ^wzmW)^g)wGU!4_6CMLIh?Eyf%(t&mccz= zT5Vs{tQx7zHaW}%3+NL!WP60}w$s3qPVI3RrO9H-dva9Ef>K@MOP64Iq8_q^h6kJM zMjAH`f$R)NBI|Ph1+oq$9gnsUqx$$y%9>=HF$Brl;@D@v4r?#+Ta@kipJ#mU#Qnc^ zuby?v?v;P!Z37Zm?k}1;#1#q#%P{?3Xo3YFo{YcuYL%onr>Qd~=@#knU?V3~PUNyL zv6h9m?LXR;7<^$y=U2zYD-C$g;V}R9vD6Llk z*$j0-AQg5udYRzT-|PWS2Des^?mD0urWV$nLemJ{g7lb>h#QOG~@bRcWpM4o}-R`Hd#J zM1^@Sm2pJApddOEJx_@{#ML|W>%DjvX8vxZ-PMv5M&AFD_X!XI++fpGN3w9Zw{;qf z^b1)S#87_rg?_~nsmIIJfb4$XD}+Au;SNF^Pe_{8Wj!>&|LAFomLG}zu=30-BXS;j zk85GF%flX82;=)!U#KbVda)n!09Lj*zn(3#@r)>Csi!(QLF*^=Un`1wDMm&l3&_}N zodmJXMnS1rmCBAL6CHKxq62pnLD20j)aD5}`(6l0N0dq^4k>{}rpHIem2+;QoZ3?3 zW$EG5VW*21Fh3MEyzI_1p5(>mvqjX@(ejwN$7R2WXq{gL<3V{&=NqrNgz~bDISJg6 zxpTD#FXJATO}#AK?ET=(GHFCOS4*BLn#2Ip>;hSjRiW}08G3snb@{7DDcfhqS}1`V zmM=J6Xol;lP<8#Zysw&#6&^E8ChDim{K zoqhT8YOo)s>qZH8(OcA!s_0EA=N}t$74|iK;Rv~bI069f2sJ#uN4}xh%A?8dvVM}c zNT2gC5OAN^OiwiYZ3Cz_93uQSz{@{v{5{;U_yZ9vBP%)bqE*W2Jz_!BM4g4}hxFTr zNi+2AjAAh9d5m?(^`0^s3bZRsT=(MJrHq%0vu@@1ru_Nfb(`_#K6p|S?~&t_Dvt?> z<*Umz@Rg8>B@fr>HBX8wef}WF`fz-a_qq<>dh%KWb*H}z;0qkcI{w~#=jG_3ce~7o zrUPl7k{*io^fK;v2~M`Q-SordWG>+DOjLE3m!#)h(DchHDzS2kh8Z?SWNRJkYrY*d zP7c4%QaM==Uf9T;L!{HxC5GnTjuQ{y3wZ-QmapD%FIWqlNb!5`Ni#b!X84|-6bZQ% zpT6;fpu6?Wn9MmdmiAI4<{1fdTmKR=-I4>2^`X1Q&m|4~Wt!e3r@LG|b{~+8XfY)Z zZi!Pz{(f)|W^O5*w1p+gKT{n2B^6{y{v*+9+rvj_6R>Toh%TK9B@2apj;VF=HSS2#LVf@!%Q#*k|-e52KzH%d*c_@ZZ@Oq2!+ z51_{bwHp6D=-<^5Fk!s5bMb0U)KBE|QGm#An`<_ZmpE3rPYc{7WCtS%*v4@|NKr<- z?hT>{`~s3R08pwNYb=Ov$Qd9Xk%-O<_x&%_Q3v+_V$y$t zliHH#OGc*LR6!$QMnLya8_Mc$L#g&WhT>NNTiAfWxGQl%B_vMz*)cU|XHOwJNS$#KckFG0o}I|S3S zAWEWu+_93Gb_*U>5l2h}nAd^7igHPZjCLB=@BerZ;9@$BVlHA>|s7gXIevTUO~Rhtgh2{eG&(k0stB+I?7*^ HtRwyl?RP3{ literal 0 HcmV?d00001 diff --git a/internal/models/models.go b/internal/models/models.go index e4eb539..eef6a30 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -23,13 +23,14 @@ type PaginationMetadata struct { } type Image struct { - Reference string `json:"reference"` - LatestReference string `json:"latestReference,omitempty"` - Description string `json:"description,omitempty"` - Tags []string `json:"tags"` - Links []ImageLink `json:"links"` - LastModified time.Time `json:"lastModified"` - Image string `json:"image,omitempty"` + Reference string `json:"reference"` + LatestReference string `json:"latestReference,omitempty"` + Description string `json:"description,omitempty"` + Tags []string `json:"tags"` + Links []ImageLink `json:"links"` + Vulnerabilities []ImageVulnerability `json:"vulnerabilities"` + LastModified time.Time `json:"lastModified"` + Image string `json:"image,omitempty"` } type RawImage struct { @@ -56,6 +57,14 @@ type ImageLink struct { URL string `json:"url"` } +type ImageVulnerability struct { + ID int `json:"id"` + Severity string `json:"severity"` + Authority string `json:"authority"` + Description string `json:"description,omitempty"` + Link string `json:"link,omitempty"` +} + type Graph struct { Edges map[string]map[string]bool `json:"edges"` Nodes map[string]GraphNode `json:"nodes"` diff --git a/internal/registry/docker/client.go b/internal/registry/docker/client.go index 683f103..5a501a1 100644 --- a/internal/registry/docker/client.go +++ b/internal/registry/docker/client.go @@ -1,6 +1,7 @@ package docker import ( + "bytes" "context" "encoding/json" "fmt" @@ -209,6 +210,61 @@ func (c *Client) GetOrganizationOrUser(ctx context.Context, organizationOrUser s return &result, nil } +func (c *Client) GetVulnerabilityReport(ctx context.Context, repo string, digest string) (*VulnerabilityReport, error) { + body, err := json.Marshal(map[string]any{ + "query": "query imageSummariesByDigest($v1:Context!,$v2:[String!]!,$v3:ScRepositoryInput){imageSummariesByDigest(context:$v1,digests:$v2,repository:$v3){digest,sbomState,vulnerabilityReport{critical,high,medium,low,unspecified,total}}}", + "variables": map[string]any{ + "v1": map[string]any{}, + "v2": []string{ + digest, + }, + "v3": map[string]any{ + "hostName": "hub.docker.com", + "repoName": repo, + }, + }, + "operationName": "imageSummariesByDigest", + }) + if err != nil { + return nil, err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.dso.docker.com/v1/graphql", bytes.NewReader(body)) + if err != nil { + return nil, err + } + + res, err := c.Client.DoCached(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + if res.StatusCode == http.StatusNotFound { + return nil, nil + } else if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %s", res.Status) + } + + var result struct { + Data struct { + ImageSummariesByDigest []struct { + Digest string `json:"digest"` + SBOMStatae string `json:"sbomState"` + VulnerabilityReport *VulnerabilityReport `json:"vulnerabilityReport"` + } `json:"imageSummariesByDigest"` + } `json:"data"` + } + if err := json.NewDecoder(res.Body).Decode(&result); err != nil { + return nil, err + } + + if len(result.Data.ImageSummariesByDigest) != 1 { + return nil, nil + } + + return result.Data.ImageSummariesByDigest[0].VulnerabilityReport, nil +} + type Page[T any] struct { Count int `json:"count"` Next *string `json:"next"` @@ -298,3 +354,12 @@ type Entity struct { Type string `json:"type"` Badge string `json:"badge,omitempty"` } + +type VulnerabilityReport struct { + Criticial int `json:"critical"` + High int `json:"high"` + Medium int `json:"medium"` + Low int `json:"low"` + Unspecified int `json:"unspecified"` + Total int `json:"total"` +} diff --git a/internal/registry/docker/client_test.go b/internal/registry/docker/client_test.go index b9a27b0..c4ab60d 100644 --- a/internal/registry/docker/client_test.go +++ b/internal/registry/docker/client_test.go @@ -66,3 +66,18 @@ func TestClientGetRepository(t *testing.T) { json.NewEncoder(os.Stdout).Encode(repository) } + +func TestGetVulnerabilityReport(t *testing.T) { + if testing.Short() { + t.Skip() + } + + client := &Client{ + Client: httputil.NewClient(cachetest.NewCache(t), 24*time.Hour), + } + + report, err := client.GetVulnerabilityReport(context.TODO(), "traefik", "sha256:bdeec8d8ac650ff774393581757a7fbd4bcdef555acd22b265c4641b3cf2256a") + require.NoError(t, err) + + json.NewEncoder(os.Stdout).Encode(report) +} diff --git a/internal/registry/docker/utils.go b/internal/registry/docker/utils.go index 2347a9f..b9ebedb 100644 --- a/internal/registry/docker/utils.go +++ b/internal/registry/docker/utils.go @@ -15,3 +15,8 @@ func RepositoryUIPath(image oci.Reference) string { return "https://hub.docker.com/r/" + url.PathEscape(owner) + "/" + url.PathEscape(name) } + +func TagUIPath(image oci.Reference, digest string) string { + owner, name, _ := strings.Cut(image.Path, "/") + return "https://hub.docker.com/layers/" + url.PathEscape(owner) + "/" + url.PathEscape(name) + "/" + url.PathEscape(image.Tag) + "/images/" + url.PathEscape(strings.ReplaceAll(digest, ":", "-")) +} diff --git a/internal/registry/oci/client.go b/internal/registry/oci/client.go index ebc4d87..3721d5c 100644 --- a/internal/registry/oci/client.go +++ b/internal/registry/oci/client.go @@ -105,6 +105,7 @@ func (c *Client) GetManifests(ctx context.Context, image Reference) ([]Manifest, SchemaVersion: manifest.SchemaVersion, MediaType: manifest.MediaType, Annotations: make(map[string]string), + Digest: manifest.Digest, }) } @@ -122,6 +123,7 @@ func (c *Client) GetManifests(ctx context.Context, image Reference) ([]Manifest, SchemaVersion: manifest.SchemaVersion, MediaType: manifest.MediaType, Annotations: make(map[string]string), + Digest: manifest.Digest, }, }, nil } diff --git a/internal/registry/oci/models.go b/internal/registry/oci/models.go index 7bac27d..c614463 100644 --- a/internal/registry/oci/models.go +++ b/internal/registry/oci/models.go @@ -50,6 +50,7 @@ type Manifest struct { SchemaVersion int `json:"schemaVersion"` MediaType string `json:"mediaType"` Annotations map[string]string `json:"annotations"` + Digest string `json:"digest"` } func (m Manifest) SourceAnnotation() string { diff --git a/internal/store/createTablesIfNotExist.sql b/internal/store/createTablesIfNotExist.sql index 36e028b..76ecae5 100644 --- a/internal/store/createTablesIfNotExist.sql +++ b/internal/store/createTablesIfNotExist.sql @@ -52,3 +52,13 @@ CREATE TABLE IF NOT EXISTS images_graphs ( PRIMARY KEY (reference), FOREIGN KEY(reference) REFERENCES images(reference) ON DELETE CASCADE ); + +CREATE TABLE IF NOT EXISTS images_vulnerabilities ( + id INTEGER PRIMARY KEY, + reference TEXT NOT NULL, + severity TEXT NOT NULL, + authority TEXT NOT NULL, + description TEXT NOT NULL, + link TEXT NOT NULL, + FOREIGN KEY(reference) REFERENCES images(reference) ON DELETE CASCADE +) diff --git a/internal/store/store.go b/internal/store/store.go index 97a64a1..bca4a69 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -230,6 +230,25 @@ func (s *Store) InsertImage(ctx context.Context, image *models.Image) error { } } + // TODO: Removed vulnerabilities are not removed from db + for _, vulnerability := range image.Vulnerabilities { + statement, err := tx.PrepareContext(ctx, `INSERT INTO images_vulnerabilities + (reference, severity, authority, description, link) + VALUES + (?, ?, ?, ?, ?);`) + if err != nil { + tx.Rollback() + return err + } + + _, err = statement.ExecContext(ctx, image.Reference, vulnerability.Severity, vulnerability.Authority, vulnerability.Description, vulnerability.Link) + statement.Close() + if err != nil { + tx.Rollback() + return err + } + } + return tx.Commit() } @@ -276,6 +295,11 @@ func (s *Store) GetImage(ctx context.Context, reference string) (*models.Image, return nil, err } + image.Vulnerabilities, err = s.GetImageVulnerabilities(ctx, reference) + if err != nil { + return nil, err + } + return &image, nil } @@ -339,6 +363,36 @@ func (s *Store) GetImagesLinks(ctx context.Context, reference string) ([]models. return links, nil } +func (s *Store) GetImageVulnerabilities(ctx context.Context, reference string) ([]models.ImageVulnerability, error) { + statement, err := s.db.PrepareContext(ctx, `SELECT id, severity, authority, description, link FROM images_vulnerabilities WHERE reference = ?;`) + if err != nil { + return nil, err + } + defer statement.Close() + + res, err := statement.QueryContext(ctx, reference) + if err != nil { + return nil, err + } + + vulnerabilities := make([]models.ImageVulnerability, 0) + for res.Next() { + var vulnerability models.ImageVulnerability + err := res.Scan(&vulnerability.ID, &vulnerability.Severity, &vulnerability.Authority, &vulnerability.Description, &vulnerability.Link) + if err != nil { + res.Close() + return nil, err + } + vulnerabilities = append(vulnerabilities, vulnerability) + } + res.Close() + if err := res.Err(); err != nil { + return nil, err + } + + return vulnerabilities, nil +} + func (s *Store) GetTags(ctx context.Context) ([]string, error) { statement, err := s.db.PrepareContext(ctx, `SELECT DISTINCT tag FROM images_tags;`) if err != nil { diff --git a/internal/worker/worker.go b/internal/worker/worker.go index 46bf4fb..416f302 100644 --- a/internal/worker/worker.go +++ b/internal/worker/worker.go @@ -82,6 +82,7 @@ func (w *Worker) ProcessRawImage(ctx context.Context, image models.RawImage) err FullDescription: nil, ReleaseNotes: nil, Links: make([]models.ImageLink, 0), + Vulnerabilities: make([]models.ImageVulnerability, 0), Graph: image.Graph, } @@ -111,6 +112,7 @@ func (w *Worker) ProcessRawImage(ctx context.Context, image models.RawImage) err Tags: data.Tags, Image: data.Image, Links: data.Links, + Vulnerabilities: data.Vulnerabilities, LastModified: time.Now(), } if data.LatestReference != nil { diff --git a/internal/workflow/imageworkflow/data.go b/internal/workflow/imageworkflow/data.go index 1bd095c..fca0786 100644 --- a/internal/workflow/imageworkflow/data.go +++ b/internal/workflow/imageworkflow/data.go @@ -18,6 +18,7 @@ type Data struct { FullDescription *models.ImageDescription ReleaseNotes *models.ImageReleaseNotes Links []models.ImageLink + Vulnerabilities []models.ImageVulnerability Graph models.Graph } @@ -52,3 +53,14 @@ func (d *Data) InsertLinks(links []models.ImageLink) { func (d *Data) InsertLink(link models.ImageLink) { d.InsertLinks([]models.ImageLink{link}) } + +func (d *Data) InsertVulnerabilities(vulnerabilities []models.ImageVulnerability) { + d.Lock() + defer d.Unlock() + + d.Vulnerabilities = append(d.Vulnerabilities, vulnerabilities...) +} + +func (d *Data) InsertVulnerability(vulnerability models.ImageVulnerability) { + d.InsertVulnerabilities([]models.ImageVulnerability{vulnerability}) +} diff --git a/internal/workflow/imageworkflow/getdockerhubvulnerabilities.go b/internal/workflow/imageworkflow/getdockerhubvulnerabilities.go new file mode 100644 index 0000000..81d86a9 --- /dev/null +++ b/internal/workflow/imageworkflow/getdockerhubvulnerabilities.go @@ -0,0 +1,111 @@ +package imageworkflow + +import ( + "fmt" + "strings" + + "github.com/AlexGustafsson/cupdate/internal/httputil" + "github.com/AlexGustafsson/cupdate/internal/models" + "github.com/AlexGustafsson/cupdate/internal/registry/docker" + "github.com/AlexGustafsson/cupdate/internal/registry/oci" + "github.com/AlexGustafsson/cupdate/internal/workflow" +) + +func GetDockerHubVulnerabilities() workflow.Step { + return workflow.Step{ + Name: "Get Docker Hub vulnerabilities", + Main: func(ctx workflow.Context) (workflow.Command, error) { + reference, err := workflow.GetInput[oci.Reference](ctx, "reference", true) + if err != nil { + return nil, err + } + + manifests, err := workflow.GetInput[[]oci.Manifest](ctx, "manifests", true) + if err != nil { + return nil, err + } + + httpClient, err := workflow.GetInput[*httputil.Client](ctx, "httpClient", true) + if err != nil { + return nil, err + } + + // NOTE: For now, only "library" images are supported as it's unclear how + // the API works for other images + if strings.Contains(reference.Name(), "/") { + return nil, nil + } + + // TODO: For now, use the first digest of a manifest + digest := "" + for _, manifest := range manifests { + fmt.Printf("%+v\n", manifest) + if manifest.Digest != "" { + digest = manifest.Digest + break + } + } + + // NOTE: For now, to not have to perform additional queries, only look up + // manifests that include the digest upfront + if digest == "" { + return nil, nil + } + + client := &docker.Client{ + Client: httpClient, + } + + report, err := client.GetVulnerabilityReport(ctx, reference.Name(), digest) + if err != nil { + return nil, err + } + + vulnerabilities := make([]models.ImageVulnerability, 0) + + if report != nil { + for i := 0; i < report.Criticial; i++ { + vulnerabilities = append(vulnerabilities, models.ImageVulnerability{ + Severity: "criticial", + Authority: "Docker Scout", + Link: docker.TagUIPath(reference, digest), + }) + } + + for i := 0; i < report.High; i++ { + vulnerabilities = append(vulnerabilities, models.ImageVulnerability{ + Severity: "high", + Authority: "Docker Scout", + Link: docker.TagUIPath(reference, digest), + }) + } + + for i := 0; i < report.Medium; i++ { + vulnerabilities = append(vulnerabilities, models.ImageVulnerability{ + Severity: "medium", + Authority: "Docker Scout", + Link: docker.TagUIPath(reference, digest), + }) + } + + for i := 0; i < report.Low; i++ { + vulnerabilities = append(vulnerabilities, models.ImageVulnerability{ + Severity: "low", + Authority: "Docker Scout", + Link: docker.TagUIPath(reference, digest), + }) + } + + for i := 0; i < report.Unspecified; i++ { + vulnerabilities = append(vulnerabilities, models.ImageVulnerability{ + Severity: "unspecified", + Authority: "Docker Scout", + Link: docker.TagUIPath(reference, digest), + }) + } + } + + return workflow.SetOutput("vulnerabilities", vulnerabilities), nil + }, + } +} diff --git a/internal/workflow/imageworkflow/workflow.go b/internal/workflow/imageworkflow/workflow.go index e07ed2c..3cf31e1 100644 --- a/internal/workflow/imageworkflow/workflow.go +++ b/internal/workflow/imageworkflow/workflow.go @@ -66,6 +66,11 @@ func New(httpClient *httputil.Client, data *Data) workflow.Workflow { WithID("owner"). With("httpClient", httpClient). With("repository", workflow.Ref{Key: "step.repository.repository"}), + GetDockerHubVulnerabilities(). + WithID("vulnerabilities"). + With("httpClient", httpClient). + With("reference", data.ImageReference). + With("manifests", workflow.Ref{Key: "job.oci.step.manifests.manifests"}), workflow.Run(func(ctx workflow.Context) (workflow.Command, error) { repository, err := workflow.GetValue[*docker.Repository](ctx, "step.repository.repository") if err != nil { @@ -76,6 +81,17 @@ func New(httpClient *httputil.Client, data *Data) workflow.Workflow { data.FullDescription = &models.ImageDescription{ Markdown: repository.FullDescription, } + + vulnerabilities, err := workflow.GetValue[[]models.ImageVulnerability](ctx, "step.vulnerabilities.vulnerabilities") + if err != nil { + return nil, err + } + + if len(vulnerabilities) > 0 { + data.InsertVulnerabilities(vulnerabilities) + data.InsertTag("vulnerable") + } + return nil, nil }), workflow.Run(func(ctx workflow.Context) (workflow.Command, error) { diff --git a/web/api.ts b/web/api.ts index 52aa196..3541d86 100644 --- a/web/api.ts +++ b/web/api.ts @@ -28,6 +28,7 @@ export interface Image { description?: string tags: string[] links: ImageLink[] + vulnerabilities: ImageVulnerability[] image?: string lastModified: string } @@ -48,6 +49,14 @@ export interface ImageLink { url: string } +export interface ImageVulnerability { + id: number + severity: string + authority: string + description?: string + link?: string +} + export interface Graph { edges: Record> nodes: Record diff --git a/web/components/InfoTooltip.tsx b/web/components/InfoTooltip.tsx index 2ff213d..59e7953 100644 --- a/web/components/InfoTooltip.tsx +++ b/web/components/InfoTooltip.tsx @@ -11,7 +11,7 @@ export function InfoTooltip({ {icon || }

{children} diff --git a/web/components/icons/fluent-shield-error-16-filled.tsx b/web/components/icons/fluent-shield-error-16-filled.tsx new file mode 100644 index 0000000..eb4c916 --- /dev/null +++ b/web/components/icons/fluent-shield-error-16-filled.tsx @@ -0,0 +1,18 @@ +import { SVGProps } from 'react' + +export function FluentShieldError16Filled(props: SVGProps) { + return ( + + + + ) +} diff --git a/web/components/icons/fluent-shield-error-24-filled.tsx b/web/components/icons/fluent-shield-error-24-filled.tsx new file mode 100644 index 0000000..fde4b50 --- /dev/null +++ b/web/components/icons/fluent-shield-error-24-filled.tsx @@ -0,0 +1,18 @@ +import { SVGProps } from 'react' + +export function FluentShieldError24Filled(props: SVGProps) { + return ( + + + + ) +} diff --git a/web/pages/Dashboard.tsx b/web/pages/Dashboard.tsx index ca6b04e..65fc6ac 100644 --- a/web/pages/Dashboard.tsx +++ b/web/pages/Dashboard.tsx @@ -5,6 +5,7 @@ import { useImages, usePagination, useTags } from '../api' import { Badge } from '../components/Badge' import { InfoTooltip } from '../components/InfoTooltip' import { FluentChevronRight24Regular } from '../components/icons/fluent-chevron-right-24-regular' +import { FluentShieldError16Filled } from '../components/icons/fluent-shield-error-16-filled' import { FluentArrowSortDown24Filled } from '../components/icons/fluent-sort-arrow-down-24-filled' import { FluentArrowSortUp24Filled } from '../components/icons/fluent-sort-arrow-up-24-filled' import { SimpleIconsOci } from '../components/icons/simple-icons-oci' @@ -186,7 +187,15 @@ export function Dashboard(): JSX.Element { - {version(image.reference)} +

+ {version(image.reference)} + {image.vulnerabilities.length > 0 && ( + }> + {image.vulnerabilities.length} vulnerabilities + reported. + + )} +

= { 'oci-registry': "Project's OCI registry", } +function unique(previousValue: T[], currentValue: T): T[] { + if (previousValue.includes(currentValue)) { + return previousValue + } + + previousValue.push(currentValue) + return previousValue +} + export function ImageLink({ type, url, @@ -159,7 +169,16 @@ export function ImagePage(): JSX.Element {
)} {/* Image name */} -

{name(image.value.reference)}

+

+ {name(image.value.reference)} + {image.value.vulnerabilities.length > 0 && ( + } + > + {image.value.vulnerabilities.length} vulnerabilities reported. + + )} +

{/* Image version */}
{!image.value.latestReference ? ( @@ -212,6 +231,80 @@ export function ImagePage(): JSX.Element {
+ {/* Vulnerability report */} + {image.value.vulnerabilities.length > 0 && ( +
+
+

Vulnerabilities

+
    +
  • + Critical:{' '} + { + image.value.vulnerabilities.filter( + (x) => x.severity === 'critical' + ).length + } +
  • +
  • + High:{' '} + { + image.value.vulnerabilities.filter( + (x) => x.severity === 'high' + ).length + } +
  • +
  • + Medium:{' '} + { + image.value.vulnerabilities.filter( + (x) => x.severity === 'medium' + ).length + } +
  • +
  • + Low:{' '} + { + image.value.vulnerabilities.filter( + (x) => x.severity === 'low' + ).length + } +
  • +
  • + Unspecified:{' '} + { + image.value.vulnerabilities.filter( + (x) => x.severity === 'unspecified' + ).length + } +
  • +
+ +

Authorities

+
    + {image.value.vulnerabilities + .map((x) => x.authority) + .reduce(unique, []) + .map((x) => ( +
  • {x}
  • + ))} +
+ +

Links

+
+
+
+ )} + {/* Release notes */} {releaseNotes.value?.html && (
diff --git a/web/tags.ts b/web/tags.ts index 307d703..d878dc4 100644 --- a/web/tags.ts +++ b/web/tags.ts @@ -87,6 +87,11 @@ export const Tags: Tag[] = [ description: 'Outdated images', color: palette.red2, }, + { + name: 'vulnerable', + description: 'Vulnerable images', + color: palette.red2, + }, ] export const TagsByName: Record = Object.fromEntries(