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/scan/mds
Viewing File: /opt/imunify360/venv/lib/python3.11/site-packages/imav/malwarelib/scan/mds/detached.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 pwd import shutil import time from abc import ABC from dataclasses import dataclass from pathlib import Path from typing import List, Optional, Type import psutil from defence360agent.contracts.messages import ( MDSReport, Message, ScanFailed, ) from defence360agent.internals.global_scope import g from defence360agent.internals.the_sink import TheSink from defence360agent.subsys.panels import hosting_panel from defence360agent.utils import rmtree from defence360agent.utils.threads import to_thread from imav.contracts.messages import ( MalwareCleanComplete, MalwareDatabaseCleanup, MalwareDatabaseCleanupFailed, MalwareDatabaseRestore, MalwareDatabaseRestoreFailed, MalwareDatabaseScan, MalwareRestoreComplete, ) from imav.malwarelib.config import ( ExitDetachedScanType, MalwareScanResourceType, ) from imav.malwarelib.scan import ( ScanAlreadyCompleteError, ScanInfoError, ) from imav.malwarelib.scan.detached import ( DetachedDir, DetachedOperation, DetachedScan, ) from imav.malwarelib.scan.mds import MDS, MDS_PATH from imav.malwarelib.scan.mds.report import ( MalwareDatabaseHitInfo, scan_report, ) from imav.malwarelib.scan.utils import trim_file_content MDS_PID_WAIT_TIME = 30 logger = logging.getLogger(__name__) class DetachedOperationFailed(Exception): pass class MDSDetachedScanDir(DetachedDir): DETACHED_DIR = "/var/imunify360/dbscan/run/scan" ignore_file: Path report_file: Path scan_info_file: Path FILES = { **DetachedDir.FILES, "ignore_file": "ignore", "report_file": "report_file", "scan_info_file": "scan_info.json", } class MDSDetachedCleanupDir(DetachedDir): DETACHED_DIR = "/var/imunify360/dbscan/run/clean" report_file: Path FILES = { **DetachedDir.FILES, "report_file": "report_file", } class MDSDetachedRestoreDir(DetachedDir): DETACHED_DIR = "/var/imunify360/dbscan/run/restore" report_file: Path FILES = { **DetachedDir.FILES, "report_file": "report_file", } @dataclass class DbScanInfo: cmd: List[str] scan_type: str | None = None initial_path: str | None = None started: int = 0 class MDSDetachedOperation(DetachedOperation, ABC): def get_detached_process_state(self, start_time=0) -> str: return super().get_detached_process_state(start_time=start_time) class MDSDetachedScan(MDSDetachedOperation, DetachedScan): DETACHED_DIR_CLS = MDSDetachedScanDir @property def RESOURCE_TYPE(self): return MalwareScanResourceType.DB def _is_scan_finished(self): return self.detached_dir.done_file.exists() def _get_progress_info(self): try: with self.detached_dir.progress_file.open() as fp: return json.load(fp) except (FileNotFoundError, json.JSONDecodeError): return {} @property def progress(self): progress_info = self._get_progress_info() try: return int(float(progress_info["progress_main"])) except KeyError: return 100 if self._is_scan_finished() else 0 @property def phase(self): if self.progress == 0: return "avd scanning" return "{} scanning".format(MDS) @property def total_resources(self): try: return int(self._get_progress_info()["total_db_count"]) except KeyError: return 0 def _load_scan_info(self) -> Optional[DbScanInfo]: try: with self.detached_dir.scan_info_file.open() as fp: info = json.load(fp) started = self.detached_dir.path.stat().st_mtime except (FileNotFoundError, json.JSONDecodeError): return None return DbScanInfo( cmd=info["cmd"], scan_type=info["scan_type"], initial_path=info["initial_path"], started=int(started), ) def _load_single_report(self, report_file): with report_file.open() as f: return json.load(f) @classmethod def process_is_suitable(cls, proc) -> bool: if proc: cmdline = proc.cmdline() return MDS_PATH in cmdline and "--scan" in cmdline return False async def complete(self) -> MalwareDatabaseScan: if (scan_info := self._load_scan_info()) is None: raise ScanInfoError message = MalwareDatabaseScan( args=scan_info.cmd, path=scan_info.initial_path, scan_id=self.detached_id, type=scan_info.scan_type, ) try: hit_report_list = [ self._load_single_report(report_file) for report_file in self.detached_dir.path.glob("report*.json") ] except FileNotFoundError as e: raise ScanAlreadyCompleteError from e except Exception as e: logger.exception("Unable to parse MDS report") message.update_with_error(str(e)) return message if not hit_report_list: message.update_with_error("No reports found") return message for report in hit_report_list: report_msg = MDSReport(report, scan_id=self.detached_id) await g.sink.process_message(report_msg) result_report = await scan_report(hit_report_list, self.detached_id) message.update_with_report(result_report) return message async def kill_running_scan_process(self, timer=time.monotonic): error = None deadline = timer() + MDS_PID_WAIT_TIME while timer() < deadline: try: pid = self.get_pid() break except (FileNotFoundError, ValueError) as err: await asyncio.sleep(1) error = err else: logger.warning( "Cannot find the mds process to kill (%s): %r." " Assuming it's already dead.", self.detached_id, error, ) return try: proc = psutil.Process(pid) if self.process_is_suitable(proc): proc.kill() except psutil.Error as err: logger.warning( "Problem when killing the running mds process: %s", err ) async def handle_aborted_process( self, *, sink, exit_type: str = ExitDetachedScanType.ABORTED, kill: bool = True, scan_path: Optional[str] = None, scan_type: Optional[str] = None, scan_started: Optional[float] = None, cmd: Optional[List[str]] = None, out: str = "", err: str = "", ) -> None: """Removes aborted detached scan from scan_queue and writes it to DB. - Parses data about scan from scan_queue and writes it to DB - Kills scan process, if it exists - Deletes scan_dir - Processes MalwareScan and ScanFailed(in case of 'ABORTED') messages :param sink: the sink to send messages :param exit_type: 'ABORTED' by default, if stopped by user, then 'STOPPED' :param kill: try to kill a process :param scan_path: which path was scanned :param scan_type: what is the scan's type :param scan_started: when was the scan started (if known) :param cmd: command line arguments :param out: command stdout :param err: command stderr """ if kill: await self.kill_running_scan_process() scan_info = self._load_scan_info() or DbScanInfo(cmd=[]) cmd = cmd or scan_info.cmd scan_path = scan_path or scan_info.initial_path scan_type = scan_type or scan_info.scan_type scan_started = int(scan_started or scan_info.started) await sink.process_message( MalwareDatabaseScan( args=cmd, error=exit_type, path=scan_path, scan_id=self.detached_id, type=scan_type, started=scan_started, completed=int(time.time()), ) ) scan_dir = self.detached_dir if exit_type == ExitDetachedScanType.ABORTED: stdout = trim_file_content(scan_dir.log_file) or out stderr = trim_file_content(scan_dir.err_file) or err msg = ScanFailed() msg["out"] = stdout msg["err"] = stderr logger.warning( "Scan %s was aborted: %s, %s", self.detached_id, stdout, stderr ) msg["command"] = cmd msg["message"] = "aborted" msg["scan_id"] = self.detached_id msg["path"] = scan_path await sink.process_message(msg) if not scan_dir.path.is_dir(): logger.warning("No such directory: %s", scan_dir) else: rmtree(str(scan_dir)) class MDSDetachedMutableOperation(MDSDetachedOperation, ABC): """Parsing of operations that can succeed or fail for any DB hit""" SUCCESS_MSG: Type[Message] FAIL_MSG: Type[Message] async def complete(self) -> Message: message = {} try: hit_report_list = [ self._load_single_report(report) for report in self.detached_dir.path.glob("report*.json") ] message = await self._parse_report(hit_report_list) except FileNotFoundError as e: raise ScanAlreadyCompleteError from e except DetachedOperationFailed as e: message["error"] = str(e) logger.exception(f"Unable to parse MDS {self.NAME} report") return self.FAIL_MSG(error=str(e)) else: if not hit_report_list: return self.FAIL_MSG(error=f"No {self.NAME} reports found") for report in hit_report_list: report_msg = MDSReport(report, scan_id=self.detached_id) await g.sink.process_message(report_msg) message["scan_id"] = self.detached_id return self.SUCCESS_MSG(**message) @staticmethod def _load_single_report(report_file): with report_file.open() as f: return json.load(f) async def _parse_report(self, hit_report_list) -> dict: users_from_panel = set(await hosting_panel.HostingPanel().get_users()) pw_all = await to_thread(pwd.getpwall) succeeded = set() failed = set() for report in hit_report_list: if errors := report["error_list"]: logger.error(f"Errors in MDS {self.NAME}: %s", errors) raise DetachedOperationFailed(errors) if not report["rows_with_error"]: succeeded |= set( MalwareDatabaseHitInfo.from_report( report, users_from_panel, pw_all, self.detached_id ) ) else: failed |= set( MalwareDatabaseHitInfo.from_report( report, users_from_panel, pw_all, self.detached_id ) ) return {"succeeded": succeeded, "failed": failed} class MDSDetachedCleanup(MDSDetachedMutableOperation): NAME = "cleanup" DETACHED_DIR_CLS = MDSDetachedCleanupDir SUCCESS_MSG = MalwareDatabaseCleanup FAIL_MSG = MalwareDatabaseCleanupFailed @property def on_complete_message(self) -> MalwareCleanComplete: return MalwareCleanComplete( scan_id=self.detached_id, ) async def handle_aborted_process( self, *, sink: TheSink, exit_type: str = ExitDetachedScanType.ABORTED, scan_path: Optional[str] = None, ) -> None: assert ( exit_type == ExitDetachedScanType.ABORTED ), "Cleanup cannot be stopped, only aborted status is supported" # NOTE: No need to kill running process because Imunify360 does # it through systemd and we don't support MDS for Imunify AV. logger.info("Cleanup %s was %s", self.detached_id, exit_type) stdout = trim_file_content(self.detached_dir.log_file) stderr = trim_file_content(self.detached_dir.err_file) msg = MalwareDatabaseCleanupFailed( error=( f"path: {scan_path}, " f"detached_id: {self.detached_id}, " f"out: {stdout}, " f"err: {stderr}" ) ) await sink.process_message(msg) shutil.rmtree(str(self.detached_dir.path), ignore_errors=True) @classmethod def process_is_suitable(cls, proc) -> bool: if proc: cmdline = proc.cmdline() return MDS_PATH in cmdline and "--clean" in cmdline return False class MDSDetachedRestore(MDSDetachedMutableOperation): NAME = "restore" DETACHED_DIR_CLS = MDSDetachedRestoreDir SUCCESS_MSG = MalwareDatabaseRestore FAIL_MSG = MalwareDatabaseRestoreFailed @classmethod def process_is_suitable(cls, proc) -> bool: if proc: cmdline = proc.cmdline() return MDS_PATH in cmdline and "--restore" in cmdline return False @property def on_complete_message(self) -> MalwareRestoreComplete: return MalwareRestoreComplete( scan_id=self.detached_id, ) async def handle_aborted_process( self, *, sink: TheSink, exit_type: str = ExitDetachedScanType.ABORTED, scan_path: Optional[str] = None, ) -> None: assert ( exit_type == ExitDetachedScanType.ABORTED ), "Restore cannot be stopped, only aborted status is supported" logger.info("Restore %s was %s", self.detached_id, exit_type) stdout = trim_file_content(self.detached_dir.log_file) stderr = trim_file_content(self.detached_dir.err_file) msg = MalwareDatabaseRestoreFailed( error=( f"path: {scan_path}, " f"detached_id: {self.detached_id}, " f"out: {stdout}, " f"err: {stderr}" ), ) await sink.process_message(msg) shutil.rmtree(str(self.detached_dir.path), ignore_errors=True)