PNG  IHDRX cHRMz&u0`:pQ<bKGD pHYsodtIME MeqIDATxw]Wug^Qd˶ 6`!N:!@xI~)%7%@Bh&`lnjVF29gΨ4E$|>cɚ{gk= %,a KX%,a KX%,a KX%,a KX%,a KX%,a KX%, b` ǟzeאfp]<!SJmɤY޲ڿ,%c ~ع9VH.!Ͳz&QynֺTkRR.BLHi٪:l;@(!MԴ=žI,:o&N'Kù\vRmJ雵֫AWic H@" !: Cé||]k-Ha oݜ:y F())u]aG7*JV@J415p=sZH!=!DRʯvɱh~V\}v/GKY$n]"X"}t@ xS76^[bw4dsce)2dU0 CkMa-U5tvLƀ~mlMwfGE/-]7XAƟ`׮g ewxwC4\[~7@O-Q( a*XGƒ{ ՟}$_y3tĐƤatgvێi|K=uVyrŲlLӪuܿzwk$m87k( `múcE)"@rK( z4$D; 2kW=Xb$V[Ru819קR~qloѱDyįݎ*mxw]y5e4K@ЃI0A D@"BDk_)N\8͜9dz"fK0zɿvM /.:2O{ Nb=M=7>??Zuo32 DLD@D| &+֎C #B8ַ`bOb $D#ͮҪtx]%`ES`Ru[=¾!@Od37LJ0!OIR4m]GZRJu$‡c=%~s@6SKy?CeIh:[vR@Lh | (BhAMy=݃  G"'wzn޺~8ԽSh ~T*A:xR[ܹ?X[uKL_=fDȊ؂p0}7=D$Ekq!/t.*2ʼnDbŞ}DijYaȲ(""6HA;:LzxQ‘(SQQ}*PL*fc\s `/d'QXW, e`#kPGZuŞuO{{wm[&NBTiiI0bukcA9<4@SӊH*؎4U/'2U5.(9JuDfrޱtycU%j(:RUbArLֺN)udA':uGQN"-"Is.*+k@ `Ojs@yU/ H:l;@yyTn}_yw!VkRJ4P)~y#)r,D =ě"Q]ci'%HI4ZL0"MJy 8A{ aN<8D"1#IJi >XjX֔#@>-{vN!8tRݻ^)N_╗FJEk]CT՟ YP:_|H1@ CBk]yKYp|og?*dGvzنzӴzjֺNkC~AbZƷ`.H)=!QͷVTT(| u78y֮}|[8-Vjp%2JPk[}ԉaH8Wpqhwr:vWª<}l77_~{s۴V+RCģ%WRZ\AqHifɤL36: #F:p]Bq/z{0CU6ݳEv_^k7'>sq*+kH%a`0ԣisqにtү04gVgW΂iJiS'3w.w}l6MC2uԯ|>JF5`fV5m`Y**Db1FKNttu]4ccsQNnex/87+}xaUW9y>ͯ骵G{䩓Գ3+vU}~jJ.NFRD7<aJDB1#ҳgSb,+CS?/ VG J?|?,2#M9}B)MiE+G`-wo߫V`fio(}S^4e~V4bHOYb"b#E)dda:'?}׮4繏`{7Z"uny-?ǹ;0MKx{:_pÚmFמ:F " .LFQLG)Q8qN q¯¯3wOvxDb\. BKD9_NN &L:4D{mm o^tֽ:q!ƥ}K+<"m78N< ywsard5+вz~mnG)=}lYݧNj'QJS{S :UYS-952?&O-:W}(!6Mk4+>A>j+i|<<|;ر^߉=HE|V#F)Emm#}/"y GII웻Jі94+v뾧xu~5C95~ūH>c@덉pʃ1/4-A2G%7>m;–Y,cyyaln" ?ƻ!ʪ<{~h~i y.zZB̃/,雋SiC/JFMmBH&&FAbϓO^tubbb_hZ{_QZ-sύodFgO(6]TJA˯#`۶ɟ( %$&+V'~hiYy>922 Wp74Zkq+Ovn錄c>8~GqܲcWꂎz@"1A.}T)uiW4="jJ2W7mU/N0gcqܗOO}?9/wìXžΏ0 >֩(V^Rh32!Hj5`;O28؇2#ݕf3 ?sJd8NJ@7O0 b־?lldщ̡&|9C.8RTWwxWy46ah嘦mh٤&l zCy!PY?: CJyв]dm4ǜҐR޻RլhX{FƯanшQI@x' ao(kUUuxW_Ñ줮[w8 FRJ(8˼)_mQ _!RJhm=!cVmm ?sFOnll6Qk}alY}; "baӌ~M0w,Ggw2W:G/k2%R,_=u`WU R.9T"v,<\Ik޽/2110Ӿxc0gyC&Ny޽JҢrV6N ``یeA16"J³+Rj*;BϜkZPJaÍ<Jyw:NP8/D$ 011z֊Ⱳ3ι֘k1V_"h!JPIΣ'ɜ* aEAd:ݺ>y<}Lp&PlRfTb1]o .2EW\ͮ]38؋rTJsǏP@芎sF\> P^+dYJLbJ C-xϐn> ι$nj,;Ǖa FU *择|h ~izť3ᤓ`K'-f tL7JK+vf2)V'-sFuB4i+m+@My=O҈0"|Yxoj,3]:cо3 $#uŘ%Y"y죯LebqtҢVzq¼X)~>4L׶m~[1_k?kxֺQ`\ |ٛY4Ѯr!)N9{56(iNq}O()Em]=F&u?$HypWUeB\k]JɩSع9 Zqg4ZĊo oMcjZBU]B\TUd34ݝ~:7ڶSUsB0Z3srx 7`:5xcx !qZA!;%͚7&P H<WL!džOb5kF)xor^aujƍ7 Ǡ8/p^(L>ὴ-B,{ۇWzֺ^k]3\EE@7>lYBȝR.oHnXO/}sB|.i@ɥDB4tcm,@ӣgdtJ!lH$_vN166L__'Z)y&kH;:,Y7=J 9cG) V\hjiE;gya~%ks_nC~Er er)muuMg2;֫R)Md) ,¶ 2-wr#F7<-BBn~_(o=KO㭇[Xv eN_SMgSҐ BS헃D%g_N:/pe -wkG*9yYSZS.9cREL !k}<4_Xs#FmҶ:7R$i,fi!~' # !6/S6y@kZkZcX)%5V4P]VGYq%H1!;e1MV<!ϐHO021Dp= HMs~~a)ަu7G^];git!Frl]H/L$=AeUvZE4P\.,xi {-~p?2b#amXAHq)MWǾI_r`S Hz&|{ +ʖ_= (YS(_g0a03M`I&'9vl?MM+m~}*xT۲(fY*V4x@29s{DaY"toGNTO+xCAO~4Ϳ;p`Ѫ:>Ҵ7K 3}+0 387x\)a"/E>qpWB=1 ¨"MP(\xp߫́A3+J] n[ʼnӼaTbZUWb={~2ooKױӰp(CS\S筐R*JغV&&"FA}J>G֐p1ٸbk7 ŘH$JoN <8s^yk_[;gy-;߉DV{c B yce% aJhDȶ 2IdйIB/^n0tNtџdcKj4϶v~- CBcgqx9= PJ) dMsjpYB] GD4RDWX +h{y`,3ꊕ$`zj*N^TP4L:Iz9~6s) Ga:?y*J~?OrMwP\](21sZUD ?ܟQ5Q%ggW6QdO+\@ ̪X'GxN @'4=ˋ+*VwN ne_|(/BDfj5(Dq<*tNt1х!MV.C0 32b#?n0pzj#!38}޴o1KovCJ`8ŗ_"]] rDUy޲@ Ȗ-;xџ'^Y`zEd?0„ DAL18IS]VGq\4o !swV7ˣι%4FѮ~}6)OgS[~Q vcYbL!wG3 7띸*E Pql8=jT\꘿I(z<[6OrR8ºC~ډ]=rNl[g|v TMTղb-o}OrP^Q]<98S¤!k)G(Vkwyqyr޽Nv`N/e p/~NAOk \I:G6]4+K;j$R:Mi #*[AȚT,ʰ,;N{HZTGMoּy) ]%dHء9Պ䠬|<45,\=[bƟ8QXeB3- &dҩ^{>/86bXmZ]]yޚN[(WAHL$YAgDKp=5GHjU&99v簪C0vygln*P)9^͞}lMuiH!̍#DoRBn9l@ xA/_v=ȺT{7Yt2N"4!YN`ae >Q<XMydEB`VU}u]嫇.%e^ánE87Mu\t`cP=AD/G)sI"@MP;)]%fH9'FNsj1pVhY&9=0pfuJ&gޤx+k:!r˭wkl03׼Ku C &ѓYt{.O.zҏ z}/tf_wEp2gvX)GN#I ݭ߽v/ .& и(ZF{e"=V!{zW`, ]+LGz"(UJp|j( #V4, 8B 0 9OkRrlɱl94)'VH9=9W|>PS['G(*I1==C<5"Pg+x'K5EMd؞Af8lG ?D FtoB[je?{k3zQ vZ;%Ɠ,]E>KZ+T/ EJxOZ1i #T<@ I}q9/t'zi(EMqw`mYkU6;[t4DPeckeM;H}_g pMww}k6#H㶏+b8雡Sxp)&C $@'b,fPߑt$RbJ'vznuS ~8='72_`{q纶|Q)Xk}cPz9p7O:'|G~8wx(a 0QCko|0ASD>Ip=4Q, d|F8RcU"/KM opKle M3#i0c%<7׿p&pZq[TR"BpqauIp$ 8~Ĩ!8Սx\ւdT>>Z40ks7 z2IQ}ItԀ<-%S⍤};zIb$I 5K}Q͙D8UguWE$Jh )cu4N tZl+[]M4k8֦Zeq֮M7uIqG 1==tLtR,ƜSrHYt&QP윯Lg' I,3@P'}'R˪e/%-Auv·ñ\> vDJzlӾNv5:|K/Jb6KI9)Zh*ZAi`?S {aiVDԲuy5W7pWeQJk֤#5&V<̺@/GH?^τZL|IJNvI:'P=Ϛt"¨=cud S Q.Ki0 !cJy;LJR;G{BJy޺[^8fK6)=yʊ+(k|&xQ2`L?Ȓ2@Mf 0C`6-%pKpm')c$׻K5[J*U[/#hH!6acB JA _|uMvDyk y)6OPYjœ50VT K}cǻP[ $:]4MEA.y)|B)cf-A?(e|lɉ#P9V)[9t.EiQPDѠ3ϴ;E:+Օ t ȥ~|_N2,ZJLt4! %ա]u {+=p.GhNcŞQI?Nd'yeh n7zi1DB)1S | S#ًZs2|Ɛy$F SxeX{7Vl.Src3E℃Q>b6G ўYCmtկ~=K0f(=LrAS GN'ɹ9<\!a`)֕y[uՍ[09` 9 +57ts6}b4{oqd+J5fa/,97J#6yν99mRWxJyѡyu_TJc`~W>l^q#Ts#2"nD1%fS)FU w{ܯ R{ ˎ󅃏џDsZSQS;LV;7 Od1&1n$ N /.q3~eNɪ]E#oM~}v֯FڦwyZ=<<>Xo稯lfMFV6p02|*=tV!c~]fa5Y^Q_WN|Vs 0ҘދU97OI'N2'8N֭fgg-}V%y]U4 峧p*91#9U kCac_AFңĪy뚇Y_AiuYyTTYЗ-(!JFLt›17uTozc. S;7A&&<ԋ5y;Ro+:' *eYJkWR[@F %SHWP 72k4 qLd'J "zB6{AC0ƁA6U.'F3:Ȅ(9ΜL;D]m8ڥ9}dU "v!;*13Rg^fJyShyy5auA?ɩGHRjo^]׽S)Fm\toy 4WQS@mE#%5ʈfFYDX ~D5Ϡ9tE9So_aU4?Ѽm%&c{n>.KW1Tlb}:j uGi(JgcYj0qn+>) %\!4{LaJso d||u//P_y7iRJ߬nHOy) l+@$($VFIQ9%EeKʈU. ia&FY̒mZ=)+qqoQn >L!qCiDB;Y<%} OgBxB!ØuG)WG9y(Ą{_yesuZmZZey'Wg#C~1Cev@0D $a@˲(.._GimA:uyw֬%;@!JkQVM_Ow:P.s\)ot- ˹"`B,e CRtaEUP<0'}r3[>?G8xU~Nqu;Wm8\RIkբ^5@k+5(By'L&'gBJ3ݶ!/㮻w҅ yqPWUg<e"Qy*167΃sJ\oz]T*UQ<\FԎ`HaNmڜ6DysCask8wP8y9``GJ9lF\G g's Nn͵MLN֪u$| /|7=]O)6s !ĴAKh]q_ap $HH'\1jB^s\|- W1:=6lJBqjY^LsPk""`]w)󭃈,(HC ?䔨Y$Sʣ{4Z+0NvQkhol6C.婧/u]FwiVjZka&%6\F*Ny#8O,22+|Db~d ~Çwc N:FuuCe&oZ(l;@ee-+Wn`44AMK➝2BRՈt7g*1gph9N) *"TF*R(#'88pm=}X]u[i7bEc|\~EMn}P瘊J)K.0i1M6=7'_\kaZ(Th{K*GJyytw"IO-PWJk)..axӝ47"89Cc7ĐBiZx 7m!fy|ϿF9CbȩV 9V-՛^pV̌ɄS#Bv4-@]Vxt-Z, &ֺ*diؠ2^VXbs֔Ìl.jQ]Y[47gj=幽ex)A0ip׳ W2[ᎇhuE^~q흙L} #-b۸oFJ_QP3r6jr+"nfzRJTUqoaۍ /$d8Mx'ݓ= OՃ| )$2mcM*cЙj}f };n YG w0Ia!1Q.oYfr]DyISaP}"dIӗթO67jqR ҊƐƈaɤGG|h;t]䗖oSv|iZqX)oalv;۩meEJ\!8=$4QU4Xo&VEĊ YS^E#d,yX_> ۘ-e\ "Wa6uLĜZi`aD9.% w~mB(02G[6y.773a7 /=o7D)$Z 66 $bY^\CuP. (x'"J60׿Y:Oi;F{w佩b+\Yi`TDWa~|VH)8q/=9!g߆2Y)?ND)%?Ǐ`k/sn:;O299yB=a[Ng 3˲N}vLNy;*?x?~L&=xyӴ~}q{qE*IQ^^ͧvü{Huu=R|>JyUlZV, B~/YF!Y\u_ݼF{_C)LD]m {H 0ihhadd nUkf3oٺCvE\)QJi+֥@tDJkB$1!Đr0XQ|q?d2) Ӣ_}qv-< FŊ߫%roppVBwü~JidY4:}L6M7f٬F "?71<2#?Jyy4뷢<_a7_=Q E=S1И/9{+93֮E{ǂw{))?maÆm(uLE#lïZ  ~d];+]h j?!|$F}*"4(v'8s<ŏUkm7^7no1w2ؗ}TrͿEk>p'8OB7d7R(A 9.*Mi^ͳ; eeUwS+C)uO@ =Sy]` }l8^ZzRXj[^iUɺ$tj))<sbDJfg=Pk_{xaKo1:-uyG0M ԃ\0Lvuy'ȱc2Ji AdyVgVh!{]/&}}ċJ#%d !+87<;qN޼Nفl|1N:8ya  8}k¾+-$4FiZYÔXk*I&'@iI99)HSh4+2G:tGhS^繿 Kتm0 вDk}֚+QT4;sC}rՅE,8CX-e~>G&'9xpW,%Fh,Ry56Y–hW-(v_,? ; qrBk4-V7HQ;ˇ^Gv1JVV%,ik;D_W!))+BoS4QsTM;gt+ndS-~:11Sgv!0qRVh!"Ȋ(̦Yl.]PQWgٳE'`%W1{ndΗBk|Ž7ʒR~,lnoa&:ü$ 3<a[CBݮwt"o\ePJ=Hz"_c^Z.#ˆ*x z̝grY]tdkP*:97YľXyBkD4N.C_[;F9`8& !AMO c `@BA& Ost\-\NX+Xp < !bj3C&QL+*&kAQ=04}cC!9~820G'PC9xa!w&bo_1 Sw"ܱ V )Yl3+ס2KoXOx]"`^WOy :3GO0g;%Yv㐫(R/r (s } u B &FeYZh0y> =2<Ϟc/ -u= c&׭,.0"g"7 6T!vl#sc>{u/Oh Bᾈ)۴74]x7 gMӒ"d]U)}" v4co[ ɡs 5Gg=XR14?5A}D "b{0$L .\4y{_fe:kVS\\O]c^W52LSBDM! C3Dhr̦RtArx4&agaN3Cf<Ԉp4~ B'"1@.b_/xQ} _߃҉/gٓ2Qkqp0շpZ2fԫYz< 4L.Cyυι1t@鎫Fe sYfsF}^ V}N<_`p)alٶ "(XEAVZ<)2},:Ir*#m_YӼ R%a||EƼIJ,,+f"96r/}0jE/)s)cjW#w'Sʯ5<66lj$a~3Kʛy 2:cZ:Yh))+a߭K::N,Q F'qB]={.]h85C9cr=}*rk?vwV렵ٸW Rs%}rNAkDv|uFLBkWY YkX מ|)1!$#3%y?pF<@<Rr0}: }\J [5FRxY<9"SQdE(Q*Qʻ)q1E0B_O24[U'],lOb ]~WjHޏTQ5Syu wq)xnw8~)c 쫬gٲߠ H% k5dƝk> kEj,0% b"vi2Wس_CuK)K{n|>t{P1򨾜j>'kEkƗBg*H%'_aY6Bn!TL&ɌOb{c`'d^{t\i^[uɐ[}q0lM˕G:‚4kb祔c^:?bpg… +37stH:0}en6x˟%/<]BL&* 5&fK9Mq)/iyqtA%kUe[ڛKN]Ě^,"`/ s[EQQm?|XJ߅92m]G.E΃ח U*Cn.j_)Tѧj̿30ڇ!A0=͜ar I3$C^-9#|pk!)?7.x9 @OO;WƝZBFU keZ75F6Tc6"ZȚs2y/1 ʵ:u4xa`C>6Rb/Yм)^=+~uRd`/|_8xbB0?Ft||Z\##|K 0>>zxv8۴吅q 8ĥ)"6>~\8:qM}#͚'ĉ#p\׶ l#bA?)|g g9|8jP(cr,BwV (WliVxxᡁ@0Okn;ɥh$_ckCgriv}>=wGzβ KkBɛ[˪ !J)h&k2%07δt}!d<9;I&0wV/ v 0<H}L&8ob%Hi|޶o&h1L|u֦y~󛱢8fٲUsւ)0oiFx2}X[zVYr_;N(w]_4B@OanC?gĦx>мgx>ΛToZoOMp>40>V Oy V9iq!4 LN,ˢu{jsz]|"R޻&'ƚ{53ўFu(<٪9:΋]B;)B>1::8;~)Yt|0(pw2N%&X,URBK)3\zz&}ax4;ǟ(tLNg{N|Ǽ\G#C9g$^\}p?556]/RP.90 k,U8/u776s ʪ_01چ|\N 0VV*3H鴃J7iI!wG_^ypl}r*jɤSR 5QN@ iZ#1ٰy;_\3\BQQ x:WJv츟ٯ$"@6 S#qe딇(/P( Dy~TOϻ<4:-+F`0||;Xl-"uw$Цi󼕝mKʩorz"mϺ$F:~E'ҐvD\y?Rr8_He@ e~O,T.(ފR*cY^m|cVR[8 JҡSm!ΆԨb)RHG{?MpqrmN>߶Y)\p,d#xۆWY*,l6]v0h15M˙MS8+EdI='LBJIH7_9{Caз*Lq,dt >+~ّeʏ?xԕ4bBAŚjﵫ!'\Ը$WNvKO}ӽmSşذqsOy?\[,d@'73'j%kOe`1.g2"e =YIzS2|zŐƄa\U,dP;jhhhaxǶ?КZ՚.q SE+XrbOu%\GتX(H,N^~]JyEZQKceTQ]VGYqnah;y$cQahT&QPZ*iZ8UQQM.qo/T\7X"u?Mttl2Xq(IoW{R^ ux*SYJ! 4S.Jy~ BROS[V|žKNɛP(L6V^|cR7i7nZW1Fd@ Ara{詑|(T*dN]Ko?s=@ |_EvF]׍kR)eBJc" MUUbY6`~V޴dJKß&~'d3i WWWWWW
Current Directory: /opt/imunify360/venv/lib/python3.11/site-packages/imav/malwarelib/utils
Viewing File: /opt/imunify360/venv/lib/python3.11/site-packages/imav/malwarelib/utils/malware_response.py
""" This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program.  If not, see <https://www.gnu.org/licenses/>. Copyright © 2019 Cloud Linux Software Inc. This software is also available under ImunifyAV commercial license, see <https://www.imunify360.com/legal/eula> """ import asyncio import json import logging import os from dataclasses import dataclass from typing import Any, AsyncGenerator, Iterable, List from urllib.parse import quote_from_bytes, urljoin from urllib.request import Request, urlopen from defence360agent import utils from defence360agent.api import inactivity from defence360agent.contracts.config import Core from defence360agent.contracts.config import Malware as Config from defence360agent.contracts.license import LicenseCLN, LicenseError from defence360agent.internals.iaid import ( IAIDTokenError, IndependentAgentIDAPI, ) from imav.contracts.config import MalwareTune logger = logging.getLogger(__name__) if utils.OsReleaseInfo.id_like() & utils.OsReleaseInfo.DEBIAN: _CURL = "/opt/alt/curlssl/usr/bin/curl" else: _CURL = "/opt/alt/curlssl11/usr/bin/curl" _API_BASE_URL = os.environ.get("I360_MRS_API_BASE_URL", Core.API_BASE_URL) _ENDPOINT_UPLOAD = os.environ.get("I360_MRS_ENDPOINT_UPLOAD", "api/v1/upload") _ENDPOINT_CHECK = os.environ.get( "I360_MRS_ENDPOINT_CHECK", "api/v1/check-known-hashes" ) _POST_FILE_TIMEOUT = int( os.environ.get("IMUNIFY360_POST_FILE_TIMEOUT", 60 * 60) # hour ) SUBMIT_TIMEOUT = 5 # seconds FALSE_NEGATIVE = "false_negative" FALSE_POSITIVE = "false_positive" UNKNOWN_REASON = "unknown" DEFAULT_CHUNK_SIZE = 1000 class UploadFailure(Exception): """Base for upload failures""" class ClientUploadError(UploadFailure): """A client error during file upload""" class ConnectionError(ClientUploadError): """A connection error during file upload""" class TimeoutError(ClientUploadError): """A timeout during file upload""" class FileTooLargeError(ClientUploadError): """File is too large to be uploaded""" class UploadFileResponseError(ClientUploadError): def __init__(self, response): super().__init__() self.response = response def __str__(self): return f"failed with response: {self.response}" class MalwareHitPath(os.PathLike): """ Wrapper that is used to send a file whose original contents may be located in a different path. """ def __init__(self, content_path: str, real_path: str | None = None): self._content_path = content_path self._real_path = real_path @property def content_path(self): return self._content_path def __eq__(self, other): if isinstance(other, MalwareHitPath): return ( self._content_path == other._content_path and self._real_path == other._real_path ) return self._content_path == other def __str__(self): if self._real_path is not None: return self._real_path return self.content_path def __repr__(self): return self.__str__() def __fspath__(self): return self.__str__() def _token_to_headers(): token = LicenseCLN.get_token() headers = { "I360-Id": token["id"], "I360-Limit": token["limit"], "I360-Status": token["status"], "I360-Token-Expire-Utc": token["token_expire_utc"], "I360-Token-Created-Utc": token["token_created_utc"], "I360-Sign": token["sign"], } headers = {key: str(value) for key, value in headers.items()} return headers async def _post_file( file: str | MalwareHitPath, url, headers=None, timeout=None ): """ Post *file* as multipart/form-data to *url* with given HTTP *headers*. Return server response as bytes (http body). Raise TimeoutError on timeout. Raise ConnectionError if failed to connect to host. Raise ClientUploadError on error. """ if headers is None: headers = {} headers_args = [ b"-H%s: %s" % (header.encode("ascii"), value.encode("latin-1")) for header, value in headers.items() ] content_path = file if isinstance(file, MalwareHitPath): content_path = file.content_path quoted_full_path = quote_from_bytes(os.fsencode(file), safe="").encode( "ascii" ) cmd = ( [os.fsencode(_CURL)] + headers_args + [b"--max-time", str(timeout).encode("ascii")] * (timeout is not None) + [ b"--form", # https://curl.haxx.se/docs/knownbugs.html#multipart_formposts_file_name_en b'file=@"%s";filename="%s"' % ( # escape backslash, double quotes os.fsencode(content_path) .replace(b"\\", b"\\\\") .replace(b'"', b'\\"'), quoted_full_path, ), b"--fail", # disable progress meter b"--silent", b"--show-error", url.encode("ascii"), ] ) rc, out, err = await utils.run(cmd) if rc != 0: if rc == 28: raise TimeoutError("Upload timed out") elif rc == 26: raise ClientUploadError(file) else: def _safe_text(value): if isinstance(value, bytes): return repr(value)[2:-1] # strip b'' quotes return repr(value) safe_msg = ("Failed to post file to {url}: {err}").format( url=_safe_text(url), err=_safe_text(err), ) if rc == 7: raise ConnectionError(safe_msg) else: raise ClientUploadError(safe_msg) return out async def upload_file( file: str | MalwareHitPath, upload_reason=UNKNOWN_REASON ): """ Upload a file to Malware Response Service. :param file: path to file :param upload_reason: one of 'unknown', 'false_positive', 'false_negative' :return: dict representing json response :raises LicenseError: """ if not LicenseCLN.is_valid(): raise LicenseError( "File uploading to Malware Responce Serivce " "requires a valid license" ) content_path = ( file.content_path if isinstance(file, MalwareHitPath) else file ) file_size = os.path.getsize(content_path) if file_size > Config.MAX_MRS_UPLOAD_FILE: raise FileTooLargeError( "File {} is {} bytes, files larger than {} bytes " "are not allowed.".format( file, file_size, Config.MAX_MRS_UPLOAD_FILE ) ) url = urljoin(_API_BASE_URL, _ENDPOINT_UPLOAD) headers = { **_token_to_headers(), "I360-Upload-Reason": upload_reason, } response_body = await _post_file( file, url, headers, timeout=_POST_FILE_TIMEOUT ) result = json.loads(response_body.decode()) logger.info( "Uploaded file %r to the Malware Response Service with reason: %s." " More" " info:" " https://blog.imunify360.com/malware-protection-powered-by-imunify-cloudav", file, upload_reason, ) if result.get("status") != "ok": raise UploadFileResponseError(result) return result async def notify_after_timeout( future: asyncio.Future, timeout: float ) -> tuple[bool, asyncio.Future]: """ Wait for the future to complete for the specified timeout. Returns (timed_out, shielded_future) where: - timed_out: True if timeout occurred, False if future completed - shielded_future: The shielded future that can be awaited later Does not cancel the future on timeout. """ shielded = asyncio.shield(future) try: await asyncio.wait_for(shielded, timeout) return False, shielded except asyncio.TimeoutError: logger.warning("Upload task timed out. Will continue in background.") shielded.add_done_callback( lambda fut: utils.log_future_errors( fut, logger.warning, "Background upload task failed after timeout", ) ) return True, shielded async def _run_retries_in_background(file, upload_reason, initial_error): """Helper coroutine to run the retry loop in the background.""" logger.warning( "Initial upload failed for %s, reason: %s. Starting retry mechanism in" " background.", file, initial_error, ) inactivity.track.start("mrs-background-upload") try: # Check if file still exists before starting retries content_path = ( file.content_path if isinstance(file, MalwareHitPath) else file ) if not os.path.exists(content_path): logger.info( "File %s no longer exists. Cancelling background upload" " retries.", file, ) return delays = [0.5, 2.5, 6, 15, 40, 100, 200] max_tries = len(delays) + 1 error = initial_error for i, pause in enumerate(delays, start=1): logger.warning( "Background retry attempt %d/%d for file %s. Retrying in %s" " seconds", i, max_tries, file, pause, ) await asyncio.sleep(pause) # Check if file still exists before each retry attempt if not os.path.exists(content_path): logger.info( "File %s was deleted during retry. Cancelling background" " upload.", file, ) return error = await _try_upload( file, raise_errors=False, upload_reason=upload_reason ) if not error: logger.info( "Background upload for %s succeeded on retry.", file ) return if error: logger.error( "Background upload for %s failed after all retries. Final" " error: %s", file, error, ) finally: inactivity.track.stop("mrs-background-upload") async def upload_with_retries( file: str | MalwareHitPath, upload_reason=UNKNOWN_REASON, notify_timeout=None, ): """ :param file: File to upload :param upload_reason: Reason for upload :param notify_timeout: Time in seconds after which to notify as pending but continue upload :raises LicenseError, ClientUploadError, ConnectionError, FileTooLargeError, """ content_path = ( file.content_path if isinstance(file, MalwareHitPath) else file ) if not os.path.exists(content_path): raise ClientUploadError(f"File {content_path} does not exist.") inactivity.track.start("mrs-upload") try: error = await _try_upload( file, raise_errors=False, upload_reason=upload_reason, notify_timeout=notify_timeout, ) except asyncio.CancelledError: raise finally: inactivity.track.stop("mrs-upload") if isinstance(error, ConnectionError): logger.warning(f"Connection error for {file}, retrying.") inactivity.track.start("mrs-upload-retry") try: error = await _try_upload( file, raise_errors=False, upload_reason=upload_reason, notify_timeout=notify_timeout, ) except asyncio.CancelledError: raise finally: inactivity.track.stop("mrs-upload-retry") if not error: return None if isinstance(error, TimeoutError): raise error if isinstance(error, FileTooLargeError): logger.warning("File %s is too big. Will not retry.", file) raise error if isinstance(error, (ConnectionError, ClientUploadError)): loop = asyncio.get_event_loop() loop.create_task( _run_retries_in_background(file, upload_reason, error) ) raise TimeoutError( "Initial connection failed. Retrying in background." ) if error: raise error return None async def _try_upload( file: str | MalwareHitPath, raise_errors, *, upload_reason=UNKNOWN_REASON, notify_timeout=None, ): """Return error instead of raising it unless *raise_errors* is true. :param notify_timeout: If set, returns TimeoutError after this many seconds but continues upload :raises LicenseError: :raises ClientUploadError, TimeoutError, ConnectionError, FileTooLargeError: if raise_errors is True """ try: if notify_timeout is not None: upload_task = asyncio.create_task( upload_file(file, upload_reason=upload_reason) ) timed_out, shielded = await notify_after_timeout( upload_task, notify_timeout ) if timed_out: error = TimeoutError("Upload notification timeout") error.args = ("Upload notification timeout",) return error await shielded else: await upload_file(file, upload_reason=upload_reason) except ( ClientUploadError, ConnectionError, FileTooLargeError, TimeoutError, ) as e: logger.debug("Failed to upload file %s. Error: %s", file, e) if raise_errors: raise return e return None @dataclass class HitInfo: file: str hash: str async def check_known_hashes( loop: asyncio.AbstractEventLoop, hashes: Iterable[str], upload_reason=UNKNOWN_REASON, chunk_size=DEFAULT_CHUNK_SIZE, ) -> AsyncGenerator[List[str], None]: hashes = list(hashes) # Usually, this condition is true only in unit and rpm-tests if MalwareTune.NO_CHECK_KNOWN_HASHES: logger.error("NO_CHECK_KNOWN_HASHES is enabled, skipping check") yield hashes return try: token = await IndependentAgentIDAPI.get_token() except IAIDTokenError as e: logger.warning("Failed to acquire IAID token: %s", e) return url = urljoin(_API_BASE_URL, _ENDPOINT_CHECK) headers = { "X-Auth": token, "I360-Upload-Reason": upload_reason, "Content-Type": "application/json", } chunks = utils.split_for_chunk(hashes, chunk_size) with utils.timeit("Check known hashes", log=logger.info): for chunk in chunks: request = {"hashes": chunk} with utils.timeit(f"Check {len(chunk)} hashes", log=logger.info): try: result = await _do_request( loop, Request( url, data=json.dumps(request).encode(), headers=headers, method="POST", ), ) yield result["unknown_hashes"] except Exception as e: logger.warning("Failed to check known hashes: %s", e) @utils.retry_on( Exception, on_error=utils.backoff_sleep, timeout=utils.HTTP_REQUEST_RETRY_TIMEOUT, ) async def _do_request( loop: asyncio.AbstractEventLoop, request: Request ) -> dict[str, Any]: return await loop.run_in_executor( None, _do_request_sync, request, ) def _do_request_sync(request: Request) -> dict[str, Any]: logger.info("Requesting %s", request.full_url) with urlopen(request, timeout=Core.DEFAULT_SOCKET_TIMEOUT) as response: if response.status != 200: logger.warning("HTTP response status code is %s", response.status) raise Exception("status code is {}".format(response.status)) return json.loads(response.read().decode())