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/plugins
Viewing File: /opt/imunify360/venv/lib/python3.11/site-packages/imav/malwarelib/plugins/store.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 functools import glob import json import os import pwd import re import time from collections import defaultdict import dataclasses from enum import Enum from logging import getLogger from typing import Any, Union import peewee import defence360agent.internals.logger from defence360agent.api import inactivity from defence360agent.contracts.messages import MessageType from defence360agent.contracts.plugins import ( MessageSink, MessageSource, expect, ) from defence360agent.model.simplification import run_in_executor from defence360agent.subsys.panels.hosting_panel import HostingPanel from defence360agent.utils import Scope, nice_iterator from imav.contracts.messages import ( MalwareDatabaseCleanup, MalwareDatabaseScan, MalwareScan, ) from imav.contracts.plugins import ProcessOrder from defence360agent.contracts.hook_events import HookEvent from imav.malwarelib.config import ( CLEANUP, CLEANUP_ON_SCHEDULE, NOTIFY, MalwareEvent, MalwareEventPostponed, MalwareHitStatus, MalwareScanResourceType, MalwareScanType, ) from imav.malwarelib.model import ( MalwareHit, MalwareHitAlternate, VulnerabilityHit, ) from imav.malwarelib.model import ( MalwareScan as MalwareScanModel, ) from imav.malwarelib.plugins.detached_scan import ( MalwareScanMessageInfo, ) from imav.malwarelib.scan.mds.report import MalwareDatabaseHitInfo from imav.malwarelib.subsys.malware import ( HackerTrapHitsSaver, MalwareAction, MalwareActionIm360, ) from imav.malwarelib.model import MalwareIgnorePath logger = getLogger(__name__) class MalwareScanJSONEncoder(json.JSONEncoder): def default(self, o: Any) -> Any: if isinstance(o, Enum): return o.value if isinstance(o, set): return list(o) if dataclasses.is_dataclass(o): return dataclasses.asdict(o) return super().default(o) class StoreMalwareHits(MessageSink, MessageSource): PROCESSING_ORDER = ProcessOrder.STORE_SCAN SCOPE = Scope.AV malware_action = MalwareAction _loop, _sink = None, None async def create_source(self, loop, sink): # type: ignore self._loop = loop self._sink = sink async def create_sink(self, loop): pass async def _call_scan_finished_hook(self, *, summary: dict): """Emit MalwareScanningFinished using summary dict, including perf stats.""" status = "failed" if summary.get("error") else "ok" stats_keys = ( "scan_time", "scan_time_hs", "scan_time_preg", "smart_time_hs", "smart_time_preg", "finder_time", "cas_time", "deobfuscate_time", "mem_peak", ) stats = {"total_files": summary.get("total_files")} for key in stats_keys: if key in summary: stats[key] = summary[key] # Ensure path is a single string (not a list) for the Finished hook event_path = summary.get("path") if isinstance(event_path, list): event_path = event_path[0] if event_path else "" finished_event = HookEvent.MalwareScanningFinished( scan_id=summary.get("scanid"), scan_type=summary.get("type"), path=event_path, started=summary.get("started"), total_files=summary.get("total_files"), total_malicious=summary.get("total_malicious", 0), error=summary.get("error"), status=status, stats=stats, scan_params=summary.get("scan_args") or {}, ) if self._sink: await self._sink.process_message(finished_event) @expect(MessageType.MalwareScan, async_lock=False) async def process_hits(self, message): """MalwareScan is saved to DB when: 1. Detached scan started - message has no results 2. Any scan finished - message has summary and results Message without summary means that detached scan is finished and summary will arrive along with results in another message. """ if not message["summary"].get("path"): return with inactivity.track.task("store_scan"): await self._store_scan(message) @expect(MessageType.MalwareScan) async def store_log(self, message): if message.get("results") is None: return summary = message.get("summary", {}) scan_id = summary.get("scanid") if not scan_id: logger.error( "MalwareScan message received without a scanid: %s", message ) return with defence360agent.internals.logger.openAibolitActionsLog( scan_id ) as logf: json.dump( dict(message), logf, indent=2, sort_keys=False, cls=MalwareScanJSONEncoder, ) @staticmethod def _store_hit(scanid, filename, status, resource_type, data): return MalwareHit.create( scanid=scanid, resource_type=resource_type, owner=data["owner"], user=data["user"], size=data["size"], hash=data["hash"], orig_file=filename, type=data["hits"][0]["matches"], timestamp=data["hits"][0]["timestamp"], status=status, malicious=not data["hits"][0]["suspicious"], ) @staticmethod def get_outdated_entries( path_obj: Union[str, list], scan_type: str | None = None, ): """ Return files that may already not be infected, yet we still consider them such. For example, an infected file might have been removed manually. """ possibly_infected_statuses = [MalwareHitStatus.FOUND] paths = [path_obj] if isinstance(path_obj, str) else path_obj if scan_type == MalwareScanType.REALTIME: # to avoid duplicates (DEF-10404) yield from iter(paths) return for target_path in paths: for path in glob.iglob(target_path): path = os.path.realpath(path) if ( os.path.isfile(path) and MalwareHit.select() .where( (MalwareHit.orig_file == path) & (MalwareHit.status.in_(possibly_infected_statuses)) & ( MalwareHit.resource_type == MalwareScanResourceType.FILE.value ) ) .first() ): yield path else: scanned_dir = re.escape(path) + r"(/.*|\b)" yield from ( i.orig_file for i in MalwareHit.select().where( (MalwareHit.orig_file.regexp(scanned_dir)) & ( MalwareHit.status.in_( possibly_infected_statuses ) ) & ( MalwareHit.resource_type == MalwareScanResourceType.FILE.value ) ) ) async def _store_scan(self, message: MalwareScan) -> None: """Process scan message results. message: MalwareScan message """ summary = message["summary"] if not summary["started"]: # Scan is queued/aborted. return message_type = MalwareScanMessageInfo(message) if message_type.is_summary: if not ( MalwareScanModel.select() .where(MalwareScanModel.scanid == message["summary"]["scanid"]) .exists() ): scan = MalwareScanModel.create( **summary, resource_type=MalwareScanResourceType.FILE.value, initiator=message.initiator, ) scan.total_malicious = 0 scan.save() else: logger.warning( "Scan %s already in database", message["summary"]["scanid"] ) else: await self._store_scan_from_results(message) @classmethod def _delete_outdated_entries(cls, summary: dict) -> None: file_patterns = summary.pop("file_patterns", None) exclude_patterns = summary.pop("exclude_patterns", None) if ( summary.get("error") is None and file_patterns is None and exclude_patterns is None ): outdated_entries = cls.get_outdated_entries( summary["path"], scan_type=summary["type"] ) MalwareHit.delete_hits(outdated_entries) @staticmethod async def _process_default_action_results( hit_data, default_action_results ): pass async def _store_scan_from_results(self, message: MalwareScan): summary = message["summary"] scan_id = summary["scanid"] scan, created = MalwareScanModel.get_or_create( scanid=scan_id, defaults={ **summary, "resource_type": MalwareScanResourceType.FILE.value, }, ) if not created: # Detached scan only (second message). # Update completed time if scan already exists. scan.completed = summary["completed"] # get('path') indicates that this is the second message, # even if they are out of order if message["results"] is not None and summary.get("path") is not None: self._delete_outdated_entries(summary) # vulnerabilities processed in a separate plugin results = { file: data for file, data in message["results"].items() if not VulnerabilityHit.match(data["hits"][0]["matches"]) } hits = { hit.orig_file: hit for hit in MalwareHit.get_hits(files=list(results)) } postponed_hits = defaultdict(list) # type: dict total_malicious = 0 def _hit_status_race_detected(hit: MalwareHit, detected_timestamp): return ( hit.status == MalwareHitStatus.CLEANUP_STARTED or hit.status in ( MalwareHitStatus.CLEANUP_DONE, MalwareHitStatus.CLEANUP_REMOVED, ) and hit.cleaned_at > detected_timestamp ) # ignore hits are already processed by another scan # to avoid send its to CH multiple times async for file in nice_iterator(tuple(results.keys())): if file in hits and _hit_status_race_detected( hits[file], results[file]["hits"][0]["timestamp"] ): results.pop(file, None) # ignore it in further processing of the message message["results"].pop(file, None) # filter out ignored paths before applying default actions async for file in nice_iterator(tuple(results.keys())): try: if await MalwareIgnorePath.is_path_ignored(file): results.pop(file, None) message["results"].pop(file, None) except Exception as exc: # be conservative: if ignore check fails, do not drop the hit logger.exception( "Ignore check failed for file %s: %s; keeping hit", file, exc, ) malicious_hits = [ MalwareHitAlternate.create(scan.scanid, file, data) for file, data in results.items() if not data["hits"][0]["suspicious"] ] action_results = await self.malware_action.apply_default_action( hits=malicious_hits, initiator=message.get("initiator"), cause=summary["type"], sink=self._sink, ) apply_dict = {} for hit_info, event, action, try_restore in action_results: apply_dict[hit_info.orig_file] = (event, action, try_restore) for file, data in results.items(): # do not store suspicious hits if data["hits"][0]["suspicious"]: continue status = MalwareHitStatus.FOUND result = None if file in apply_dict: ( result, default_action, try_restore, ) = apply_dict[file] # sent to CH if ( isinstance(result, MalwareEventPostponed) and result.action == CLEANUP_ON_SCHEDULE ): # report to CH only well-known `cleanup` / `notify` actions default_action = ( CLEANUP if summary["type"] == MalwareScanType.BACKGROUND else NOTIFY ) data["default_action"] = default_action data["try_restore"] = try_restore total_malicious += 1 if isinstance(result, MalwareEvent): if result.malware_eliminated: continue hit = await run_in_executor( self._loop, functools.partial( self._store_hit, scan.scanid, file, status, MalwareScanResourceType.FILE.value, data, ), ) if isinstance(result, MalwareEventPostponed): key = ( result.message, ( result.cause, result.initiator, result.post_action, result.action, ), ) postponed_hits[key].append(hit) scan.total_malicious = total_malicious scan.total_resources = summary["total_files"] scan.timestamp = int(time.time()) if error := summary.get("error"): scan.error = error scan.save() # Emit MalwareScanningFinished after persistence completes. try: summary["total_malicious"] = scan.total_malicious await self._call_scan_finished_hook(summary=summary) except Exception: logger.exception( "Failed to emit MalwareScanningFinished after store (DB," " no hits)" ) if self._sink: for ( (msg_cls, (cause, initiator, post_action, action)), hits, ) in postponed_hits.items(): if ( action == CLEANUP_ON_SCHEDULE and summary["type"] != MalwareScanType.BACKGROUND ): logger.info( "Skipping auto-cleanup because it's allowed for " "scheduled scans only" ) else: await self._sink.process_message( msg_cls( hits=hits, scan_id=scan_id, cause=cause, initiator=initiator, post_action=post_action, ) ) await self._process_default_action_results( results, {hit.orig_file: event for hit, event, _, _ in action_results}, ) class StoreMalwareHitsIm360(StoreMalwareHits): SCOPE = Scope.IM360 malware_action = MalwareActionIm360 async def create_sink(self, loop): await super().create_sink(loop) await HackerTrapHitsSaver.init() @staticmethod async def _process_default_action_results( hit_data, default_action_results ): """Do additional processing for malicious files""" hacker_trap_hits = [] hacker_trap_sa_hits = [] for path, data in hit_data.items(): result = default_action_results.get(path) if not isinstance(result, MalwareEvent): continue if result.malware_eliminated: hacker_trap_hits.append(path) if any( HackerTrapHitsSaver.STANDALONE_MARK in hit["matches"] for hit in data["hits"] ): hacker_trap_sa_hits.append(path) await HackerTrapHitsSaver.add_hits(hacker_trap_hits) await HackerTrapHitsSaver.update_sa_hits(hacker_trap_sa_hits, []) @expect(MessageType.MalwareDatabaseScan) async def store_db_scan(self, message: MalwareDatabaseScan) -> None: if not message.started or message.type is None: # Scan is queued/aborted or stopped while AVD is not finished yet return try: scan = MalwareScanModel.create( scanid=message.scan_id, started=message.started, completed=message.completed, type=message.type, path=message.path, error=message.error, total_resources=message.total_resources, total_malicious=message.total_malicious, resource_type=MalwareScanResourceType.DB.value, initiator=message.initiator, ) except peewee.IntegrityError: scan = MalwareScanModel.get(scanid=message.scan_id) if ( not message.completed or message.error or (scan.completed and not scan.error) or not scan.completed or message.path != scan.path or message.type != scan.type or scan.resource_type != MalwareScanResourceType.DB.value ): logger.error( "The scan %s has already been saved: type=%s, path=%s," " completed=%s", scan.scanid, scan.resource_type, scan.path, scan.completed, ) return # Update scan with latest data from message scan.started = message.started scan.completed = message.completed scan.error = message.error scan.total_resources = message.total_resources scan.total_malicious = message.total_malicious scan.resource_type = MalwareScanResourceType.DB.value scan.initiator = message.initiator scan.save() logger.info( f"Updated scan {scan.scanid} with new data from message" ) scan_id = message.get("scan_id") if scan_id: with defence360agent.internals.logger.openMdsActionsLog( scan_id ) as logf: json.dump( dict(message), logf, indent=2, sort_keys=False, cls=MalwareScanJSONEncoder, ) if not message.hits: # no malware found if not message.error: # remove outdated entries as rescan did not find anything MalwareHit.delete().where( (MalwareHit.orig_file == message.path) & ( MalwareHit.resource_type == MalwareScanResourceType.DB.value ) & (MalwareHit.status == MalwareHitStatus.FOUND) ).execute() # Emit MalwareScanningFinished for DB scans without hits try: summary = { "scanid": message.scan_id, "type": message.type, "path": message.path, "started": message.started, "completed": message.completed, "total_files": message.total_resources, "total_malicious": message.total_malicious or 0, "error": message.error, "scan_args": {}, } await self._call_scan_finished_hook(summary=summary) except Exception: logger.exception( "Failed to emit MalwareScanningFinished after store (DB," " no hits)" ) return # FIXME: remove this mapping # when we start to store UID instead of username in the db panel_users = set(await HostingPanel().get_users()) uid_to_name = { pw.pw_uid: pw.pw_name for pw in pwd.getpwall() if pw.pw_name in panel_users } self._delete_outdated_db_entries(message.hits) # apply default action to all hits (to store them in history table) action_results = await self.malware_action.apply_default_action( hits=message.hits, initiator=message.get("initiator"), cause=message.get("type"), sink=self._sink, resource_type=MalwareScanResourceType.DB.value, ) apply_dict = {} for hit, event, action, _ in action_results: apply_dict[hit.path] = (event, action) postponed_hits = defaultdict(list) # type: dict unique_hits_info = MalwareDatabaseHitInfo.get_hits_per_db(message.hits) for hit_info in unique_hits_info: result = None if hit_info.path in apply_dict: ( result, default_action, ) = apply_dict[hit_info.path] # FIXME: DEF-18112 add default_action to hit and send to CH if isinstance(result, MalwareEvent): if result.malware_eliminated: continue new_hit: MalwareHit = MalwareHit.create( scanid=scan, owner=uid_to_name.get(hit_info.owner, hit_info.owner), user=uid_to_name.get(hit_info.user, hit_info.user), orig_file=hit_info.path, type=hit_info.signature, malicious=True, hash=None, size=None, timestame=None, status=MalwareHitStatus.FOUND, cleaned_at=None, resource_type=MalwareScanResourceType.DB.value, app_name=hit_info.app_name, db_host=hit_info.db_host, db_port=hit_info.db_port, db_name=hit_info.db_name, snippet=hit_info.snippet, ) if isinstance(result, MalwareEventPostponed): key = ( result.message, (result.cause, result.initiator, result.post_action), ) postponed_hits[key].append(new_hit) if self._sink: # Emit MalwareScanningFinished for DB scans with hits before posting postponed events try: summary = { "scanid": message.scan_id, "type": message.type, "path": message.path, "started": message.started, "completed": message.completed, "total_files": message.total_resources, "total_malicious": message.total_malicious or 0, "error": message.error, "scan_args": {}, } await self._call_scan_finished_hook(summary=summary) except Exception: logger.exception( "Failed to emit MalwareScanningFinished after store (DB," " hits)" ) for ( (msg_cls, (cause, initiator, post_action)), hits, ) in postponed_hits.items(): await self._sink.process_message( msg_cls( hits=hits, scan_id=message.scan_id, cause=cause, initiator=initiator, post_action=post_action, ) ) @staticmethod def _delete_outdated_db_entries(hits): orig_files = [hit.path for hit in hits] MalwareHit.delete_hits(orig_files) @expect(MessageType.MalwareDatabaseCleanup) async def store_db_cleanup_log(self, message: MalwareDatabaseCleanup): scan_id = message.get("scan_id") if not scan_id: logger.error( "MalwareDatabaseCleanup message received without a" " scan_id: %s", message, ) return with defence360agent.internals.logger.openMdsActionsLog( scan_id ) as logf: json.dump( dict(message), logf, indent=2, sort_keys=False, cls=MalwareScanJSONEncoder, )