PNG  IHDRQgAMA a cHRMz&u0`:pQ<bKGDgmIDATxwUﹻ& ^CX(J I@ "% (** BX +*i"]j(IH{~R)[~>h{}gy)I$Ij .I$I$ʊy@}x.: $I$Ii}VZPC)I$IF ^0ʐJ$I$Q^}{"r=OzI$gRZeC.IOvH eKX $IMpxsk.쒷/&r[޳<v| .I~)@$updYRa$I |M.e JaֶpSYR6j>h%IRز if&uJ)M$I vLi=H;7UJ,],X$I1AҒJ$ XY XzI@GNҥRT)E@;]K*Mw;#5_wOn~\ DC&$(A5 RRFkvIR}l!RytRl;~^ǷJj اy뷦BZJr&ӥ8Pjw~vnv X^(I;4R=P[3]J,]ȏ~:3?[ a&e)`e*P[4]T=Cq6R[ ~ޤrXR Հg(t_HZ-Hg M$ãmL5R uk*`%C-E6/%[t X.{8P9Z.vkXŐKjgKZHg(aK9ڦmKjѺm_ \#$5,)-  61eJ,5m| r'= &ڡd%-]J on Xm|{ RҞe $eڧY XYrԮ-a7RK6h>n$5AVڴi*ֆK)mѦtmr1p| q:흺,)Oi*ֺK)ܬ֦K-5r3>0ԔHjJئEZj,%re~/z%jVMڸmrt)3]J,T K֦OvԒgii*bKiNO~%PW0=dii2tJ9Jݕ{7"I P9JKTbu,%r"6RKU}Ij2HKZXJ,妝 XYrP ެ24c%i^IK|.H,%rb:XRl1X4Pe/`x&P8Pj28Mzsx2r\zRPz4J}yP[g=L) .Q[6RjWgp FIH*-`IMRaK9TXcq*I y[jE>cw%gLRԕiFCj-ďa`#e~I j,%r,)?[gp FI˨mnWX#>mʔ XA DZf9,nKҲzIZXJ,L#kiPz4JZF,I,`61%2s $,VOϚ2/UFJfy7K> X+6 STXIeJILzMfKm LRaK9%|4p9LwJI!`NsiazĔ)%- XMq>pk$-$Q2x#N ؎-QR}ᶦHZډ)J,l#i@yn3LN`;nڔ XuX5pF)m|^0(>BHF9(cզEerJI rg7 4I@z0\JIi䵙RR0s;$s6eJ,`n 䂦0a)S)A 1eJ,堌#635RIgpNHuTH_SԕqVe ` &S)>p;S$魁eKIuX`I4춒o}`m$1":PI<[v9^\pTJjriRŭ P{#{R2,`)e-`mgj~1ϣLKam7&U\j/3mJ,`F;M'䱀 .KR#)yhTq;pcK9(q!w?uRR,n.yw*UXj#\]ɱ(qv2=RqfB#iJmmL<]Y͙#$5 uTU7ӦXR+q,`I}qL'`6Kͷ6r,]0S$- [RKR3oiRE|nӦXR.(i:LDLTJjY%o:)6rxzҒqTJjh㞦I.$YR.ʼnGZ\ֿf:%55 I˼!6dKxm4E"mG_ s? .e*?LRfK9%q#uh$)i3ULRfK9yxm܌bj84$i1U^@Wbm4uJ,ҪA>_Ij?1v32[gLRD96oTaR׿N7%L2 NT,`)7&ƝL*꽙yp_$M2#AS,`)7$rkTA29_Iye"|/0t)$n XT2`YJ;6Jx".e<`$) PI$5V4]29SRI>~=@j]lp2`K9Jaai^" Ԋ29ORI%:XV5]JmN9]H;1UC39NI%Xe78t)a;Oi Ҙ>Xt"~G>_mn:%|~ޅ_+]$o)@ǀ{hgN;IK6G&rp)T2i୦KJuv*T=TOSV>(~D>dm,I*Ɛ:R#ۙNI%D>G.n$o;+#RR!.eU˽TRI28t)1LWϚ>IJa3oFbu&:tJ*(F7y0ZR ^p'Ii L24x| XRI%ۄ>S1]Jy[zL$adB7.eh4%%누>WETf+3IR:I3Xה)3אOۦSRO'ٺ)S}"qOr[B7ϙ.edG)^ETR"RtRݜh0}LFVӦDB^k_JDj\=LS(Iv─aTeZ%eUAM-0;~˃@i|l @S4y72>sX-vA}ϛBI!ݎߨWl*)3{'Y|iSlEڻ(5KtSI$Uv02,~ԩ~x;P4ցCrO%tyn425:KMlD ^4JRxSهF_}شJTS6uj+ﷸk$eZO%G*^V2u3EMj3k%)okI]dT)URKDS 7~m@TJR~荪fT"֛L \sM -0T KfJz+nإKr L&j()[E&I ߴ>e FW_kJR|!O:5/2跌3T-'|zX ryp0JS ~^F>-2< `*%ZFP)bSn"L :)+pʷf(pO3TMW$~>@~ū:TAIsV1}S2<%ޟM?@iT ,Eūoz%i~g|`wS(]oȤ8)$ ntu`өe`6yPl IzMI{ʣzʨ )IZ2= ld:5+請M$-ї;U>_gsY$ÁN5WzWfIZ)-yuXIfp~S*IZdt;t>KūKR|$#LcԀ+2\;kJ`]YǔM1B)UbG"IRߊ<xܾӔJ0Z='Y嵤 Leveg)$znV-º^3Ւof#0Tfk^Zs[*I꯳3{)ˬW4Ւ4 OdpbZRS|*I 55#"&-IvT&/윚Ye:i$ 9{LkuRe[I~_\ؠ%>GL$iY8 9ܕ"S`kS.IlC;Ҏ4x&>u_0JLr<J2(^$5L s=MgV ~,Iju> 7r2)^=G$1:3G< `J3~&IR% 6Tx/rIj3O< ʔ&#f_yXJiގNSz; Tx(i8%#4 ~AS+IjerIUrIj362v885+IjAhK__5X%nV%Iͳ-y|7XV2v4fzo_68"S/I-qbf; LkF)KSM$ Ms>K WNV}^`-큧32ŒVؙGdu,^^m%6~Nn&͓3ŒVZMsRpfEW%IwdǀLm[7W&bIRL@Q|)* i ImsIMmKmyV`i$G+R 0tV'!V)֏28vU7͒vHꦼtxꗞT ;S}7Mf+fIRHNZUkUx5SAJㄌ9MqμAIRi|j5)o*^'<$TwI1hEU^c_j?Е$%d`z cyf,XO IJnTgA UXRD }{H}^S,P5V2\Xx`pZ|Yk:$e ~ @nWL.j+ϝYb퇪bZ BVu)u/IJ_ 1[p.p60bC >|X91P:N\!5qUB}5a5ja `ubcVxYt1N0Zzl4]7­gKj]?4ϻ *[bg$)+À*x쳀ogO$~,5 زUS9 lq3+5mgw@np1sso Ӻ=|N6 /g(Wv7U;zωM=wk,0uTg_`_P`uz?2yI!b`kĸSo+Qx%!\οe|އԁKS-s6pu_(ֿ$i++T8=eY; צP+phxWQv*|p1. ά. XRkIQYP,drZ | B%wP|S5`~́@i޾ E;Չaw{o'Q?%iL{u D?N1BD!owPHReFZ* k_-~{E9b-~P`fE{AܶBJAFO wx6Rox5 K5=WwehS8 (JClJ~ p+Fi;ŗo+:bD#g(C"wA^ r.F8L;dzdIHUX݆ϞXg )IFqem%I4dj&ppT{'{HOx( Rk6^C٫O.)3:s(۳(Z?~ٻ89zmT"PLtw䥈5&b<8GZ-Y&K?e8,`I6e(֍xb83 `rzXj)F=l($Ij 2*(F?h(/9ik:I`m#p3MgLaKjc/U#n5S# m(^)=y=đx8ŬI[U]~SцA4p$-F i(R,7Cx;X=cI>{Km\ o(Tv2vx2qiiDJN,Ҏ!1f 5quBj1!8 rDFd(!WQl,gSkL1Bxg''՞^ǘ;pQ P(c_ IRujg(Wz bs#P­rz> k c&nB=q+ؔXn#r5)co*Ũ+G?7< |PQӣ'G`uOd>%Mctz# Ԫڞ&7CaQ~N'-P.W`Oedp03C!IZcIAMPUۀ5J<\u~+{9(FbbyAeBhOSܳ1 bÈT#ŠyDžs,`5}DC-`̞%r&ڙa87QWWp6e7 Rϫ/oY ꇅ Nܶըtc!LA T7V4Jsū I-0Pxz7QNF_iZgúWkG83 0eWr9 X]㾮݁#Jˢ C}0=3ݱtBi]_ &{{[/o[~ \q鯜00٩|cD3=4B_b RYb$óBRsf&lLX#M*C_L܄:gx)WΘsGSbuL rF$9';\4Ɍq'n[%p.Q`u hNb`eCQyQ|l_C>Lb꟟3hSb #xNxSs^ 88|Mz)}:](vbۢamŖ࿥ 0)Q7@0=?^k(*J}3ibkFn HjB׻NO z x}7p 0tfDX.lwgȔhԾŲ }6g E |LkLZteu+=q\Iv0쮑)QٵpH8/2?Σo>Jvppho~f>%bMM}\//":PTc(v9v!gոQ )UfVG+! 35{=x\2+ki,y$~A1iC6#)vC5^>+gǵ@1Hy٪7u;p psϰu/S <aʸGu'tD1ԝI<pg|6j'p:tպhX{o(7v],*}6a_ wXRk,O]Lܳ~Vo45rp"N5k;m{rZbΦ${#)`(Ŵg,;j%6j.pyYT?}-kBDc3qA`NWQū20/^AZW%NQ MI.X#P#,^Ebc&?XR tAV|Y.1!؅⨉ccww>ivl(JT~ u`ٵDm q)+Ri x/x8cyFO!/*!/&,7<.N,YDŽ&ܑQF1Bz)FPʛ?5d 6`kQձ λc؎%582Y&nD_$Je4>a?! ͨ|ȎWZSsv8 j(I&yj Jb5m?HWp=g}G3#|I,5v珿] H~R3@B[☉9Ox~oMy=J;xUVoj bUsl_35t-(ՃɼRB7U!qc+x4H_Qo֮$[GO<4`&č\GOc[.[*Af%mG/ ňM/r W/Nw~B1U3J?P&Y )`ѓZ1p]^l“W#)lWZilUQu`-m|xĐ,_ƪ|9i:_{*(3Gѧ}UoD+>m_?VPۅ15&}2|/pIOʵ> GZ9cmíتmnz)yߐbD >e}:) r|@R5qVSA10C%E_'^8cR7O;6[eKePGϦX7jb}OTGO^jn*媓7nGMC t,k31Rb (vyܴʭ!iTh8~ZYZp(qsRL ?b}cŨʊGO^!rPJO15MJ[c&~Z`"ѓޔH1C&^|Ш|rʼ,AwĴ?b5)tLU)F| &g٣O]oqSUjy(x<Ϳ3 .FSkoYg2 \_#wj{u'rQ>o;%n|F*O_L"e9umDds?.fuuQbIWz |4\0 sb;OvxOSs; G%T4gFRurj(֍ڑb uԖKDu1MK{1^ q; C=6\8FR艇!%\YÔU| 88m)֓NcLve C6z;o&X x59:q61Z(T7>C?gcļxѐ Z oo-08jہ x,`' ҔOcRlf~`jj".Nv+sM_]Zk g( UOPyεx%pUh2(@il0ݽQXxppx-NS( WO+轾 nFߢ3M<;z)FBZjciu/QoF 7R¥ ZFLF~#ȣߨ^<쩡ݛкvџ))ME>ώx4m#!-m!L;vv#~Y[đKmx9.[,UFS CVkZ +ߟrY٧IZd/ioi$%͝ب_ֶX3ܫhNU ZZgk=]=bbJS[wjU()*I =ώ:}-蹞lUj:1}MWm=̛ _ ¾,8{__m{_PVK^n3esw5ӫh#$-q=A̟> ,^I}P^J$qY~Q[ Xq9{#&T.^GVj__RKpn,b=`żY@^՝;z{paVKkQXj/)y TIc&F;FBG7wg ZZDG!x r_tƢ!}i/V=M/#nB8 XxЫ ^@CR<{䤭YCN)eKOSƟa $&g[i3.C6xrOc8TI;o hH6P&L{@q6[ Gzp^71j(l`J}]e6X☉#͕ ׈$AB1Vjh㭦IRsqFBjwQ_7Xk>y"N=MB0 ,C #o6MRc0|$)ف"1!ixY<B9mx `,tA>)5ػQ?jQ?cn>YZe Tisvh# GMމȇp:ԴVuږ8ɼH]C.5C!UV;F`mbBk LTMvPʍϤj?ԯ/Qr1NB`9s"s TYsz &9S%U԰> {<ؿSMxB|H\3@!U| k']$U+> |HHMLޢ?V9iD!-@x TIî%6Z*9X@HMW#?nN ,oe6?tQwڱ.]-y':mW0#!J82qFjH -`ѓ&M0u Uγmxϵ^-_\])@0Rt.8/?ٰCY]x}=sD3ojަЫNuS%U}ԤwHH>ڗjܷ_3gN q7[q2la*ArǓԖ+p8/RGM ]jacd(JhWko6ڎbj]i5Bj3+3!\j1UZLsLTv8HHmup<>gKMJj0@H%,W΃7R) ">c, xixј^ aܖ>H[i.UIHc U1=yW\=S*GR~)AF=`&2h`DzT󑓶J+?W+}C%P:|0H܆}-<;OC[~o.$~i}~HQ TvXΈr=b}$vizL4:ȰT|4~*!oXQR6Lk+#t/g lԁߖ[Jڶ_N$k*". xsxX7jRVbAAʯKҎU3)zSNN _'s?f)6X!%ssAkʱ>qƷb hg %n ~p1REGMHH=BJiy[<5 ǁJҖgKR*倳e~HUy)Ag,K)`Vw6bRR:qL#\rclK/$sh*$ 6덤 KԖc 3Z9=Ɣ=o>X Ώ"1 )a`SJJ6k(<c e{%kϊP+SL'TcMJWRm ŏ"w)qc ef꒵i?b7b('"2r%~HUS1\<(`1Wx9=8HY9m:X18bgD1u ~|H;K-Uep,, C1 RV.MR5άh,tWO8WC$ XRVsQS]3GJ|12 [vM :k#~tH30Rf-HYݺ-`I9%lIDTm\ S{]9gOڒMNCV\G*2JRŨ;Rҏ^ڽ̱mq1Eu?To3I)y^#jJw^Ńj^vvlB_⋌P4x>0$c>K†Aļ9s_VjTt0l#m>E-,,x,-W)سo&96RE XR.6bXw+)GAEvL)͞K4$p=Ũi_ѱOjb HY/+@θH9޼]Nԥ%n{ &zjT? Ty) s^ULlb,PiTf^<À] 62R^V7)S!nllS6~͝V}-=%* ʻ>G DnK<y&>LPy7'r=Hj 9V`[c"*^8HpcO8bnU`4JȪAƋ#1_\ XϘHPRgik(~G~0DAA_2p|J묭a2\NCr]M_0 ^T%e#vD^%xy-n}-E\3aS%yN!r_{ )sAw ڼp1pEAk~v<:`'ӭ^5 ArXOI驻T (dk)_\ PuA*BY]yB"l\ey hH*tbK)3 IKZ򹞋XjN n *n>k]X_d!ryBH ]*R 0(#'7 %es9??ښFC,ՁQPjARJ\Ρw K#jahgw;2$l*) %Xq5!U᢯6Re] |0[__64ch&_}iL8KEgҎ7 M/\`|.p,~`a=BR?xܐrQ8K XR2M8f ?`sgWS%" Ԉ 7R%$ N}?QL1|-эټwIZ%pvL3Hk>,ImgW7{E xPHx73RA @RS CC !\ȟ5IXR^ZxHл$Q[ŝ40 (>+ _C >BRt<,TrT {O/H+˟Pl6 I B)/VC<6a2~(XwV4gnXR ϱ5ǀHٻ?tw똤Eyxp{#WK qG%5],(0ӈH HZ])ג=K1j&G(FbM@)%I` XRg ʔ KZG(vP,<`[ Kn^ SJRsAʠ5xՅF`0&RbV tx:EaUE/{fi2;.IAwW8/tTxAGOoN?G}l L(n`Zv?pB8K_gI+ܗ #i?ޙ.) p$utc ~DžfՈEo3l/)I-U?aԅ^jxArA ΧX}DmZ@QLےbTXGd.^|xKHR{|ΕW_h] IJ`[G9{).y) 0X YA1]qp?p_k+J*Y@HI>^?gt.06Rn ,` ?);p pSF9ZXLBJPWjgQ|&)7! HjQt<| ؅W5 x W HIzYoVMGP Hjn`+\(dNW)F+IrS[|/a`K|ͻ0Hj{R,Q=\ (F}\WR)AgSG`IsnAR=|8$}G(vC$)s FBJ?]_u XRvύ6z ŨG[36-T9HzpW̞ú Xg큽=7CufzI$)ki^qk-) 0H*N` QZkk]/tnnsI^Gu't=7$ Z;{8^jB% IItRQS7[ϭ3 $_OQJ`7!]W"W,)Iy W AJA;KWG`IY{8k$I$^%9.^(`N|LJ%@$I}ֽp=FB*xN=gI?Q{٥4B)mw $Igc~dZ@G9K X?7)aK%݅K$IZ-`IpC U6$I\0>!9k} Xa IIS0H$I H ?1R.Чj:4~Rw@p$IrA*u}WjWFPJ$I➓/6#! LӾ+ X36x8J |+L;v$Io4301R20M I$-E}@,pS^ޟR[/s¹'0H$IKyfŸfVOπFT*a$I>He~VY/3R/)>d$I>28`Cjw,n@FU*9ttf$I~<;=/4RD~@ X-ѕzἱI$: ԍR a@b X{+Qxuq$IЛzo /~3\8ڒ4BN7$IҀj V]n18H$IYFBj3̵̚ja pp $Is/3R Ӻ-Yj+L;.0ŔI$Av? #!5"aʄj}UKmɽH$IjCYs?h$IDl843.v}m7UiI=&=0Lg0$I4: embe` eQbm0u? $IT!Sƍ'-sv)s#C0:XB2a w I$zbww{."pPzO =Ɔ\[ o($Iaw]`E).Kvi:L*#gР7[$IyGPI=@R 4yR~̮´cg I$I/<tPͽ hDgo 94Z^k盇΄8I56^W$I^0̜N?4*H`237}g+hxoq)SJ@p|` $I%>-hO0eO>\ԣNߌZD6R=K ~n($I$y3D>o4b#px2$yڪtzW~a $I~?x'BwwpH$IZݑnC㧄Pc_9sO gwJ=l1:mKB>Ab<4Lp$Ib o1ZQ@85b̍ S'F,Fe,^I$IjEdù{l4 8Ys_s Z8.x m"+{~?q,Z D!I$ϻ'|XhB)=…']M>5 rgotԎ 獽PH$IjIPhh)n#cÔqA'ug5qwU&rF|1E%I$%]!'3AFD/;Ck_`9 v!ٴtPV;x`'*bQa w I$Ix5 FC3D_~A_#O݆DvV?<qw+I$I{=Z8".#RIYyjǪ=fDl9%M,a8$I$Ywi[7ݍFe$s1ՋBVA?`]#!oz4zjLJo8$I$%@3jAa4(o ;p,,dya=F9ً[LSPH$IJYЉ+3> 5"39aZ<ñh!{TpBGkj}Sp $IlvF.F$I z< '\K*qq.f<2Y!S"-\I$IYwčjF$ w9 \ߪB.1v!Ʊ?+r:^!I$BϹB H"B;L'G[ 4U#5>੐)|#o0aڱ$I>}k&1`U#V?YsV x>{t1[I~D&(I$I/{H0fw"q"y%4 IXyE~M3 8XψL}qE$I[> nD?~sf ]o΁ cT6"?'_Ἣ $I>~.f|'!N?⟩0G KkXZE]ޡ;/&?k OۘH$IRۀwXӨ<7@PnS04aӶp.:@\IWQJ6sS%I$e5ڑv`3:x';wq_vpgHyXZ 3gЂ7{{EuԹn±}$I$8t;b|591nءQ"P6O5i }iR̈́%Q̄p!I䮢]O{H$IRϻ9s֧ a=`- aB\X0"+5"C1Hb?߮3x3&gşggl_hZ^,`5?ߎvĸ%̀M!OZC2#0x LJ0 Gw$I$I}<{Eb+y;iI,`ܚF:5ܛA8-O-|8K7s|#Z8a&><a&/VtbtLʌI$I$I$I$I$I$IRjDD%tEXtdate:create2022-05-31T04:40:26+00:00!Î%tEXtdate:modify2022-05-31T04:40:26+00:00|{2IENDB` sh-3ll

HOME


sh-3ll 1.0
DIR:/proc/thread-self/root/opt/imunify360/venv/lib64/python3.11/site-packages/imav/wordpress/
Upload File :
Current File : //proc/thread-self/root/opt/imunify360/venv/lib64/python3.11/site-packages/imav/wordpress/plugin.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
import pwd
import shutil
import time
import zipfile
from collections import defaultdict
from distutils.version import LooseVersion
from pathlib import Path

import yaml

from defence360agent.api import inactivity
from defence360agent.contracts.config import (
    MalwareScanScheduleInterval as Interval,
    SystemConfig,
    ANTIVIRUS_MODE,
)
from defence360agent.files import Index
from defence360agent.sentry import log_message
from defence360agent.utils import check_run
from imav import files
from imav.contracts.config import Wordpress
from imav.model.wordpress import WPSite
from imav.wordpress import cli, telemetry, PLUGIN_VERSION_FILE
from imav.malwarelib.plugins.schedule_watcher import get_user_schedule_config
from imav.wordpress.utils import (
    build_command_for_user,
    calculate_next_scan_timestamp,
    clear_get_cagefs_enabled_users_cache,
    get_last_scan,
    get_malware_history,
    prepare_scan_data,
    write_plugin_data_file_atomically,
)
from imav.wordpress.site_repository import (
    delete_site,
    get_outdated_sites,
    get_sites_for_user,
    get_sites_to_install,
    get_sites_to_mark_as_manually_deleted,
    get_installed_sites,
    insert_installed_sites,
    mark_site_as_manually_deleted,
    update_site_version,
)

from imav.wordpress.proxy_auth import setup_site_authentication

logger = logging.getLogger(__name__)

COMPONENTS_DB_PATH = Path(
    "/var/lib/cloudlinux-app-version-detector/components_versions.sqlite3"
)

# WordPress rules file names
WP_RULES_ZIP_FILENAME = "wp-rules.zip"
WP_RULES_VERSION_FILENAME = "VERSION"


def clear_caches():
    """Clear all WordPress-related caches."""
    clear_get_cagefs_enabled_users_cache()
    cli.clear_get_content_dir_cache()


def site_search(items: dict, user_info: pwd.struct_passwd, matcher) -> dict:
    # Get all WordPress sites for the user (the main site is always last)
    user_sites = get_sites_for_user(user_info)
    result = {path: [] for path in user_sites}
    for item in items:
        # Find all matching sites for this item
        matching_sites = [path for path in user_sites if matcher(item, path)]

        if matching_sites:
            # Find the most specific (longest) matching path
            most_specific_site = max(matching_sites, key=len)
            result[most_specific_site].append(item)

    return result


async def _get_scan_data_for_user(
    sink, user_info: pwd.struct_passwd, admin_config: SystemConfig
):
    # Get the last scan data
    last_scan = await get_last_scan(sink, user_info.pw_name)

    # Extract the last scan date
    last_scan_time = last_scan.get("scan_date", None)

    # Get user-specific schedule configuration
    interval, hour, day_of_month, day_of_week = get_user_schedule_config(
        user_info.pw_name, admin_config
    )

    next_scan_time = None
    if interval != Interval.NONE:
        next_scan_time = calculate_next_scan_timestamp(
            interval, hour, day_of_month, day_of_week
        )

    # Get the malware history for the user
    malware_history = get_malware_history(user_info.pw_name)

    # Split malware history by site. This part relies on the main site being the last one in the list.
    # Without this all malware could be attributed to the main site.
    malware_by_site = site_search(
        malware_history,
        user_info,
        lambda item, path: item["resource_type"] == "file"
        and item["file"].startswith(path),
    )

    return last_scan_time, next_scan_time, malware_by_site


async def _send_telemetry_task(coro, semaphore: asyncio.Semaphore):
    async with semaphore:
        try:
            await coro
        except Exception as e:
            logger.error(f"Telemetry task failed: {e}")


async def process_telemetry_tasks(coroutines: list, concurrency=10):
    """
    Process a list of telemetry coroutines with a concurrency limit.s
    """
    if not coroutines:
        return

    semaphore = asyncio.Semaphore(concurrency)
    tasks = [
        asyncio.create_task(_send_telemetry_task(coro, semaphore))
        for coro in coroutines
    ]

    try:
        await asyncio.gather(*tasks)
    except Exception as e:
        logger.error(f"Some telemetry tasks failed: {e}")


async def install_everywhere(sink):
    """Install the imunify-security plugin for all sites where it is not installed."""
    logger.info("Installing imunify-security wp plugin")

    # Keep track of the installed sites
    installed = set()
    authenticated = set()
    rules_installed = set()
    failed_rules_updates = set()
    failed = set()
    telemetry_coros = []
    with inactivity.track.task("wp-plugin-installation"):
        try:
            clear_caches()

            to_install = get_sites_to_install()

            if not to_install:
                return

            # Create SystemConfig once for all users
            admin_config = SystemConfig()

            # Create wp rules once for all users
            try:
                wp_rules_index = files.Index(
                    files.WP_RULES, integrity_check=False
                )
                await wp_rules_index.update()
                wp_rules_data = get_updated_wp_rules_data(wp_rules_index)
            except Exception as e:
                logger.warning(
                    "Failed to load wp-rules index: %s, skipping rules"
                    " installation",
                    e,
                )
                wp_rules_data = None

            if not wp_rules_data:
                logger.warning(
                    "valid WordPress rules not found, skipping rules"
                    " installation"
                )

            if wp_rules_data:
                # Get version and create ruleset dict with version and rules
                wp_rules_version = get_wp_ruleset_version(wp_rules_index)
                ruleset_dict = {
                    "version": wp_rules_version,
                    "rules": wp_rules_data,
                }
                wp_rules_php = _format_php_with_embedded_json(ruleset_dict)
            else:
                wp_rules_php = None

            # Group sites by user id
            sites_by_user = defaultdict(list)
            for site in to_install:
                sites_by_user[site.uid].append(site)

            # Now iterate over the grouped sites
            for uid, sites in sites_by_user.items():
                try:
                    user_info = pwd.getpwuid(uid)
                    username = user_info.pw_name
                except Exception as error:
                    log_message(
                        "Skipping installation of WordPress plugin on"
                        " {count} site(s) because they belong to user"
                        " {user} and it is not possible to retrieve"
                        " username for this user. Reason: {reason}",
                        format_args={
                            "count": len(sites),
                            "user": uid,
                            "reason": error,
                        },
                        level="warning",
                        component="wordpress",
                        fingerprint="wp-plugin-install-skip-user",
                    )
                    continue

                (
                    last_scan_time,
                    next_scan_time,
                    malware_by_site,
                ) = await _get_scan_data_for_user(
                    sink, user_info, admin_config
                )

                for site in sites:
                    if await remove_site_if_missing(sink, site):
                        continue
                    try:
                        # Check if site is correctly installed and accessible using WP CLI
                        is_wordpress_installed = (
                            await cli.is_wordpress_installed(site)
                        )
                        if not is_wordpress_installed:
                            log_message(
                                "WordPress site is not accessible using WP"
                                " CLI. site={site}",
                                format_args={"site": site},
                                level="warning",
                                component="wordpress",
                                fingerprint="wp-plugin-cli-not-accessible",
                            )
                            continue

                        # Prepare scan data
                        scan_data = prepare_scan_data(
                            last_scan_time,
                            next_scan_time,
                            username,
                            site,
                            malware_by_site,
                        )

                        # Create the scan data file
                        await update_scan_data_file(site, scan_data)
                        await update_site_auth(
                            site, user_info, authenticated, failed
                        )

                        # Install the plugin
                        await cli.plugin_install(site)

                        # Install rules
                        if wp_rules_php:
                            await update_wp_rules_for_site(
                                site,
                                user_info,
                                wp_rules_php,
                                rules_installed,
                                failed_rules_updates,
                            )

                        # Get the version of the plugin
                        version = await cli.get_plugin_version(site)

                        if not version:
                            installed.add(site)
                        else:
                            installed.add(
                                WPSite.build_with_version(site, version)
                            )

                        # Prepare telemetry
                        telemetry_coros.append(
                            telemetry.send_event(
                                sink=sink,
                                event="installed_by_imunify",
                                site=site,
                                version=version,
                            )
                        )
                    except Exception as error:
                        logger.error(
                            "Failed to install plugin to site=%s error=%r",
                            site,
                            error,
                        )
            logger.info(
                "Installed imunify-security wp plugin on %d sites",
                len(installed),
            )
            if failed:
                logger.warning(
                    "Failed to authenticate %d sites",
                    len(failed),
                )
            if failed_rules_updates:
                logger.warning(
                    "Failed to install wp-rules on %d sites",
                    len(failed_rules_updates),
                )
        except asyncio.CancelledError:
            logger.info(
                "Installation imunify-security wp plugin was cancelled. Plugin"
                " was installed for %d sites",
                len(installed),
            )
        except Exception as error:
            logger.error(
                "Error occurred during plugin installation. error=%r", error
            )
            raise
        finally:
            # Insert the installed sites into the database
            insert_installed_sites(installed)
            # Send telemetry
            await process_telemetry_tasks(telemetry_coros)

    return installed


def get_latest_plugin_version() -> str:
    """Get the latest version of the imunify-security plugin from the version file."""
    try:
        if not PLUGIN_VERSION_FILE.exists():
            logger.error(
                "Plugin version file does not exist: %s", PLUGIN_VERSION_FILE
            )
            return None
        return PLUGIN_VERSION_FILE.read_text().strip()
    except Exception as e:
        logger.error("Failed to read plugin version file: %s", e)
        return None


async def update_everywhere(sink):
    """Update the imunify-security plugin on all sites where it is installed."""
    latest_version = get_latest_plugin_version()
    if not latest_version:
        logger.error("Could not determine latest plugin version")
        return

    logger.info(
        "Updating imunify-security wp plugin to the latest version %s",
        latest_version,
    )

    updated = set()
    telemetry_coros = []
    with inactivity.track.task("wp-plugin-update"):
        try:
            # Get sites with outdated versions
            outdated_sites = get_outdated_sites(latest_version)
            logger.info(f"Found {len(outdated_sites)} outdated sites")

            if not outdated_sites:
                return

            # Create SystemConfig once for all users
            admin_config = SystemConfig()

            # Group sites by user id
            sites_by_user = defaultdict(list)
            for site in outdated_sites:
                sites_by_user[site.uid].append(site)

            # Process each user's sites
            for uid, sites in sites_by_user.items():
                try:
                    user_info = pwd.getpwuid(uid)
                    username = user_info.pw_name
                except Exception as error:
                    logger.error(
                        "Failed to get username for uid=%d. error=%s",
                        uid,
                        error,
                    )
                    continue

                # Get scan data once for all sites of this user
                (
                    last_scan_time,
                    next_scan_time,
                    malware_by_site,
                ) = await _get_scan_data_for_user(
                    sink, user_info, admin_config
                )

                for site in sites:
                    if await remove_site_if_missing(sink, site):
                        continue
                    try:
                        # Check if site still exists
                        if not await cli.is_wordpress_installed(site):
                            logger.info(
                                "WordPress site no longer exists: %s", site
                            )
                            continue

                        # Prepare scan data
                        scan_data = prepare_scan_data(
                            last_scan_time,
                            next_scan_time,
                            username,
                            site,
                            malware_by_site,
                        )

                        # Update the scan data file
                        await update_scan_data_file(site, scan_data)

                        # Now update the plugin
                        await cli.plugin_update(site)
                        updated.add(site)

                        # Get the version after update
                        version = await cli.get_plugin_version(site)
                        if version:
                            # Store original version for comparison
                            original_version = site.version

                            # Update the database with the new version
                            update_site_version(site, version)

                            # Create a new WPSite with updated version
                            site = site.build_with_version(version)

                            # Determine if this is a downgrade
                            is_downgrade = LooseVersion(
                                version
                            ) < LooseVersion(original_version)

                            # Prepare telemetry
                            telemetry_coros.append(
                                telemetry.send_event(
                                    sink=sink,
                                    event=(
                                        "downgraded_by_imunify"
                                        if is_downgrade
                                        else "updated_by_imunify"
                                    ),
                                    site=site,
                                    version=version,
                                )
                            )

                    except Exception as error:
                        logger.error(
                            "Failed to update plugin on site=%s error=%s",
                            site,
                            error,
                        )

            logger.info(
                "Updated imunify-security wp plugin on %d sites",
                len(updated),
            )
        except asyncio.CancelledError:
            logger.info(
                "Update of imunify-security wp plugin was cancelled. Plugin"
                " was updated on %d sites",
                len(updated),
            )
        except Exception as error:
            logger.error(
                "Error occurred during plugin update. error=%s", error
            )
            raise
        finally:
            # Send telemetry
            await process_telemetry_tasks(telemetry_coros)


async def delete_plugin_files(site: WPSite):
    data_dir = await cli.get_data_dir(site)
    if data_dir.exists():
        await asyncio.to_thread(shutil.rmtree, data_dir)


async def remove_from_single_site(site: WPSite, sink, telemetry_coros) -> int:
    """
    Remove the imunify-security plugin from a single site, including all cleanup and telemetry.
    Returns the number of affected sites (should be 1 if deletion was successful).
    This function is intended to be protected with asyncio.shield to ensure it completes even if the parent task is cancelled.
    """
    try:
        # Check if site is still installed and accessible using WP CLI
        is_installed = await cli.is_plugin_installed(site)
        if not is_installed:
            # Plugin is no longer installed. It was removed manually by the user.
            await process_manually_deleted_plugin(
                site, time.time(), sink, telemetry_coros
            )
            return 0

        # Get the version of the plugin (for telemetry data)
        version = await cli.get_plugin_version(site)

        # Uninstall the plugin from WordPress site.
        await cli.plugin_uninstall(site)

        # Delete the data files from the site.
        await delete_plugin_files(site)

        # Delete the site from database.
        affected = delete_site(site)

        # Send telemetry for successful uninstall
        telemetry_coros.append(
            telemetry.send_event(
                sink=sink,
                event="uninstalled_by_imunify",
                site=site,
                version=version,
            )
        )
        return affected
    except Exception as error:
        # Log any error that occurs during the removal process
        logger.error("Failed to remove plugin from %s %s", site, error)
        return 0


async def remove_all_installed(sink):
    """Remove the imunify-security plugin from all sites where it is installed."""
    logger.info("Deleting imunify-security wp plugin")

    telemetry_coros = []
    affected = 0
    with inactivity.track.task("wp-plugin-removal"):
        try:
            clear_caches()

            to_remove = get_installed_sites()

            for site in to_remove:
                try:
                    affected += await asyncio.shield(
                        remove_from_single_site(site, sink, telemetry_coros)
                    )
                except asyncio.CancelledError:
                    logger.info(
                        "Deleting imunify-security wp plugin was cancelled."
                        " Plugin was deleted from %d sites (out of %d)",
                        affected,
                        len(to_remove),
                    )
        except Exception as error:
            logger.error("Error occurred during plugin deleting. %s", error)
            raise
        finally:
            logger.info(
                "Removed imunify-security wp plugin from %s sites",
                affected,
            )

            #  send telemetry
            await process_telemetry_tasks(telemetry_coros)


async def process_manually_deleted_plugin(site, now, sink, telemetry_coros):
    """
    Process the manually deleted plugin for a single site.

    Args:
        site: The site to process.
        now: The current time.
        sink: The telemetry/event sink.
        telemetry_coros: The list of telemetry coroutines to add the event to.

    The process includes:
    - marking the site as manually deleted in the database
    - removing plugin data files
    - sending telemetry for manual removal
    """
    try:
        # Mark the site as manually deleted in the database
        mark_site_as_manually_deleted(site, now)

        # Remove plugin data files
        await delete_plugin_files(site)

        # Send telemetry for manual removal
        telemetry_coros.append(
            telemetry.send_event(
                sink=sink,
                event="removed_by_user",
                site=site,
                version=site.version,
            )
        )
    except Exception as error:
        logger.error(
            "Failed to process manually deleted plugin for site=%s error=%s",
            site,
            error,
        )


async def tidy_up_manually_deleted(
    sink, freshly_installed_sites: set[WPSite] = None
):
    """
    Tidy up sites that have been manually deleted by the user.

    Args:
        sink: The telemetry/event sink.
        freshly_installed_sites: Optional set of sites that were just installed and should be excluded
                                from being marked as manually deleted to avoid race conditions.
    """
    telemetry_coros = []
    try:
        to_mark_as_manually_removed = get_sites_to_mark_as_manually_deleted(
            freshly_installed_sites
        )
        if to_mark_as_manually_removed:
            now = time.time()
            for site in to_mark_as_manually_removed:
                await process_manually_deleted_plugin(
                    site, now, sink, telemetry_coros
                )

    except Exception as error:
        logger.error("Error occurred during site tidy up. %s", error)
    finally:
        if telemetry_coros:
            await process_telemetry_tasks(telemetry_coros)


async def update_data_on_sites(sink, sites: list[WPSite]):
    if not sites:
        return

    # Create SystemConfig once for all users
    admin_config = SystemConfig()

    # Group sites by user id
    sites_by_user = defaultdict(list)
    for site in sites:
        sites_by_user[site.uid].append(site)

    # Now iterate over the grouped sites
    for uid, sites in sites_by_user.items():
        try:
            user_info = pwd.getpwuid(uid)
            username = user_info.pw_name
        except Exception as error:
            logger.error(
                "Failed to get username for uid=%d. error=%s",
                uid,
                error,
            )
            continue

        (
            last_scan_time,
            next_scan_time,
            malware_by_site,
        ) = await _get_scan_data_for_user(sink, user_info, admin_config)

        for site in sites:
            if await remove_site_if_missing(sink, site):
                continue
            try:
                # Prepare scan data
                scan_data = prepare_scan_data(
                    last_scan_time,
                    next_scan_time,
                    username,
                    site,
                    malware_by_site,
                )

                # Update the scan data file
                await update_scan_data_file(site, scan_data)
            except Exception as error:
                logger.error(
                    "Failed to update scan data on site=%s error=%s",
                    site,
                    error,
                )


async def update_scan_data_file(site: WPSite, scan_data: dict):
    # Get the gid for the given user
    user_info = pwd.getpwuid(site.uid)
    gid = user_info.pw_gid

    # Ensure data directory exists with correct permissions
    data_dir = await _ensure_site_data_directory(site, user_info)

    scan_data_path = data_dir / "scan_data.php"

    # Format and write the PHP file
    php_content = _format_php_with_embedded_json(scan_data)
    write_plugin_data_file_atomically(
        scan_data_path, php_content, uid=site.uid, gid=gid
    )


def _format_php_with_embedded_json(data: dict) -> str:
    """
    Format a dictionary as a PHP file that returns JSON-decoded data.

    This creates a WordPress-safe PHP file that:
    1. Checks if it's being included from WordPress (WPINC defined)
    2. Returns the data as a decoded JSON string

    Args:
        data: Dictionary to embed in the PHP file

    Returns:
        Formatted PHP file content as a string
    """
    return (
        "<?php\n"
        "if ( ! defined( 'WPINC' ) ) {\n"
        "\texit;\n"
        "}\n"
        "return json_decode( '"
        + json.dumps(data).replace("'", "\\'")
        + "', true );"
    )


def _find_file_in_index(index: Index, filename: str) -> Path | None:
    """
    Find a file path from the index by filename.

    Args:
        index: files.Index object
        filename: Name of the file to find (e.g., WP_RULES_ZIP_FILENAME, WP_RULES_VERSION_FILENAME)

    Returns:
        Path to the file or None if not found
    """
    for item in index.items():
        if item["name"] == filename:
            file_path = Path(index.localfilepath(item["url"]))

            if file_path.exists():
                return file_path
    logger.error("%s not found in %s", filename, index.files_path(index.type))
    return None


def _extract_wp_rules_yaml(zip_path: Path) -> dict | None:
    """
    Extract and parse wp-rules.yaml from the zip file.

    Args:
        zip_path: Path to wp-rules.zip file

    Returns:
        Parsed YAML data as dict or None if extraction/parsing fails
    """
    try:
        with zipfile.ZipFile(zip_path, "r") as zip_file:
            with zip_file.open("wp-rules.yaml") as yaml_file:
                rules_data = yaml.safe_load(yaml_file)
    except (zipfile.BadZipFile, KeyError, yaml.YAMLError) as e:
        logger.error("Failed to extract or parse wp-rules.yaml: %s", e)
        return None

    if not isinstance(rules_data, dict):
        logger.error("Invalid wp-rules.yaml format: %s", rules_data)
        return None
    return rules_data


async def _ensure_site_data_directory(
    site: WPSite, user_info: pwd.struct_passwd
) -> Path:
    """
    Ensure the site's data directory exists with correct permissions.

    Args:
        site: WordPress site
        user_info: User information from pwd

    Returns:
        Path to data directory

    Raises:
        Exception: If the data directory is a symlink or cannot be created
    """
    data_dir = await cli.get_data_dir(site)

    if os.path.islink(data_dir):
        raise Exception(
            "Data directory %s is a symlink, skipping.", str(data_dir)
        )

    if not data_dir.exists():
        command = build_command_for_user(
            user_info.pw_name,
            ["mkdir", "-p", str(data_dir)],
        )
        await check_run(command)

        if not data_dir.exists():
            raise Exception(
                "Failed to create directory %s for user %s",
                str(data_dir),
                user_info.pw_name,
            )

        # we can safely change the permissions of the directory because we just created it
        data_dir.chmod(0o750)

    return data_dir


def get_updated_wp_rules_data(index: Index) -> dict | None:
    """
    Retrieve the latest WordPress rules and return them as a dictionary.

    Args:
        index (Index): The files.Index object used to locate the wp-rules.zip file.

    Returns:
        dict: The parsed wp-rules data as a dictionary.
              If the wp-rules archive or data cannot be found or parsed, returns None.
    """
    # Find wp-rules.zip file
    zip_path = _find_file_in_index(index, WP_RULES_ZIP_FILENAME)
    if not zip_path:
        return None

    # Extract and parse wp-rules.yaml
    rules_data = _extract_wp_rules_yaml(zip_path)
    if not rules_data:
        return None
    logger.info("Successfully parsed wp-rules.yaml")

    if ANTIVIRUS_MODE:
        # all rules will be in monitoring mode only for AV and AV+
        for cve, params in rules_data.items():
            params["mode"] = "pass"

    return rules_data


def get_wp_ruleset_version(index: Index) -> str:
    """
    Retrieve the WordPress ruleset version string from the VERSION file.

    Args:
        index (Index): The files.Index object used to locate the VERSION file.

    Returns:
        str: The version string from the VERSION file.
             If the VERSION file cannot be found or read, returns "NA".
    """
    # Find VERSION file
    version_path = _find_file_in_index(index, WP_RULES_VERSION_FILENAME)
    if not version_path:
        return "NA"

    try:
        version_string = version_path.read_text().strip()
        logger.info("Successfully read wp-rules version: %s", version_string)
        return version_string
    except Exception as e:
        logger.error("Failed to read VERSION file: %s", e)
        return "NA"


async def update_wp_rules_for_site(
    site: WPSite,
    user_info: pwd.struct_passwd,
    wp_rules_php: str,
    updated: set,
    failed: set,
) -> None:
    """
    Deploy wp-rules to a single WordPress site and track the result.

    Args:
        site: WordPress site to deploy to
        user_info: User information from pwd
        wp_rules_php: Formatted PHP rules content
        updated: Set to add site to if successful
        failed: Set to add site to if failed
    """
    gid = user_info.pw_gid

    try:
        data_dir = await _ensure_site_data_directory(site, user_info)
        rules_path = data_dir / "rules.php"
        write_plugin_data_file_atomically(
            rules_path, wp_rules_php, uid=site.uid, gid=gid
        )
        updated.add(site)
        logger.info("Updated wp-rules for site %s", site.docroot)
    except Exception as error:
        failed.add(site)
        logger.error(
            "Failed to update wp-rules for site %s: %s",
            site.docroot,
            error,
        )


async def update_wp_rules_on_sites(index: Index, is_updated: bool) -> None:
    """
    Hook that runs when wp-rules files are updated.
    Extracts wp-rules.yaml from wp-rules.zip and deploys to all active WordPress sites.

    Args:
        index: files.Index object for wp-rules
        is_updated: Whether files were actually updated
    """
    if not Wordpress.SECURITY_PLUGIN_ENABLED:
        logger.info(
            "wordpress security plugin not enabled, skipping wp-rules"
            " deployment"
        )
        return

    if not is_updated:
        logger.info("wp-rules not updated, skipping deployment")
        return

    logger.info("Starting wp-rules deployment to WordPress sites")

    wp_rules_data = get_updated_wp_rules_data(index)
    if not wp_rules_data:
        logger.error("No valid wp-rules found, skipping deployment")
        return

    # Get version and create ruleset dict with version and rules
    wp_rules_version = get_wp_ruleset_version(index)
    ruleset_dict = {
        "version": wp_rules_version,
        "rules": wp_rules_data,
    }
    wp_rules_php = _format_php_with_embedded_json(ruleset_dict)

    updated = set()
    failed = set()

    with inactivity.track.task("wp-rules-deployment"):
        try:
            clear_caches()

            # Get all active WordPress sites
            installed_sites = get_installed_sites()
            if not installed_sites:
                logger.info("No active WordPress sites found")
                return

            sites_by_user = defaultdict(list)
            for site in installed_sites:
                sites_by_user[site.uid].append(site)

            # Process users concurrently
            tasks = []
            for uid, sites in sites_by_user.items():
                try:
                    user_info = pwd.getpwuid(uid)
                    # Create tasks for all sites of this user
                    for site in sites:
                        task = update_wp_rules_for_site(
                            site, user_info, wp_rules_php, updated, failed
                        )
                        tasks.append(task)
                except Exception as error:
                    log_message(
                        "Skipping wp-rules update for {count} site(s)"
                        " belonging to user {user} because username retrieval"
                        " failed. Reason: {reason}",
                        format_args={
                            "count": len(sites),
                            "user": uid,
                            "reason": error,
                        },
                        level="warning",
                        component="wordpress",
                        fingerprint="wp-rules-update-skip-user",
                    )
                    for site in sites:
                        failed.add(site)
                    continue

            # Run all site updates concurrently with a reasonable limit
            max_concurrent = 10
            for i in range(0, len(tasks), max_concurrent):
                batch = tasks[i : i + max_concurrent]
                await asyncio.gather(*batch, return_exceptions=True)

            logger.info(
                "wp-rules deployment complete. Updated: %d, Failed: %d",
                len(updated),
                len(failed),
            )

        except asyncio.CancelledError:
            logger.info(
                "wp-rules deployment was cancelled. Updated %d sites",
                len(updated),
            )
        except Exception as error:
            logger.error(
                "Error occurred during wp-rules deployment. error=%s", error
            )
            raise


async def update_auth_everywhere():
    """Update auth.php files for all existing WordPress sites."""
    logger.info("Updating auth.php files for existing WordPress sites")

    updated = set()
    failed = set()

    with inactivity.track.task("wp-auth-update"):
        try:
            clear_caches()

            # Get all installed sites from db
            installed_sites = get_installed_sites()

            if not installed_sites:
                logger.info("No installed WordPress sites found")
                return

            sites_by_user = defaultdict(list)
            for site in installed_sites:
                sites_by_user[site.uid].append(site)

            # Process users concurrently
            tasks = []
            for uid, sites in sites_by_user.items():
                try:
                    user_info = pwd.getpwuid(uid)
                    # Create tasks for all sites of this user
                    for site in sites:
                        task = update_site_auth(
                            site, user_info, updated, failed
                        )
                        tasks.append(task)
                except Exception as error:
                    log_message(
                        "Skipping auth update for WordPress sites on"
                        " {count} site(s) because they belong to user"
                        " {user} and it is not possible to retrieve"
                        " username for this user. Reason: {reason}",
                        format_args={
                            "count": len(sites),
                            "user": uid,
                            "reason": error,
                        },
                        level="warning",
                        component="wordpress",
                        fingerprint="wp-plugin-auth-update-skip-user",
                    )
                    continue

            # Run all site updates concurrently with a reasonable limit
            # Adjust max_concurrent based on your system's I/O capacity
            max_concurrent = 10
            for i in range(0, len(tasks), max_concurrent):
                batch = tasks[i : i + max_concurrent]
                await asyncio.gather(*batch, return_exceptions=True)

            logger.info(
                "Updated auth.php files for %d WordPress sites, %d failed",
                len(updated),
                len(failed),
            )

        except asyncio.CancelledError:
            logger.info(
                "Auth update for WordPress sites was cancelled. Auth was"
                " updated for %d sites",
                len(updated),
            )
        except Exception as error:
            logger.error("Error occurred during auth update. error=%s", error)
            raise


async def update_site_auth(site, user_info, updated, failed):
    """Process authentication setup for a single site."""
    try:
        await setup_site_authentication(site, user_info)
        updated.add(site)
    except Exception as error:
        failed.add(site)
        logger.error(
            "Failed to update auth for site=%s error=%s",
            site,
            error,
        )


async def remove_site_if_missing(sink, site: WPSite) -> bool:
    """
    Checks if the site directory exists. If not, removes the site from the local database and sends a 'site_removed' telemetry event only if deletion is successful.
    Returns True if the site was removed (directory missing), False otherwise.
    Parameters:
        sink: The telemetry/event sink.
        site: The WPSite object to check and potentially remove.
    Side effect: If the site is missing and successfully deleted from database, a telemetry event will be sent.
    """
    if os.path.isdir(site.docroot):
        return False

    # Attempt to delete the site from the database first
    rows_deleted = delete_site(site)

    # Only send telemetry if the deletion was successful (at least one row was deleted)
    if rows_deleted > 0:
        await telemetry.send_event(
            sink=sink, event="site_removed", site=site, version=site.version
        )
    else:
        logger.warning(
            "Failed to delete missing site %s from database, no rows affected",
            site,
        )

        log_message(
            "Failed to delete missing site {site} from database",
            format_args={"site": site},
            level="warning",
            component="wordpress",
            fingerprint="wp-plugin-site-delete-failed",
        )

    return True


async def fix_site_data_file_permissions(
    site: WPSite, file_permissions: int
) -> bool:
    """
    Fix data file permissions for a single WordPress site.

    Args:
        site: The WordPress site to fix permissions for
        file_permissions: The file permissions to set (e.g., 0o440 or 0o400)

    Returns:
        bool: True if permissions were fixed successfully, False otherwise
    """
    try:
        # Get the data directory
        data_dir = await cli.get_data_dir(site)
        if not data_dir.exists():
            return False

        # Fix directory permissions (0o750) only if not already correct
        current_dir_mode = data_dir.stat().st_mode & 0o777
        if current_dir_mode != 0o750:
            data_dir.chmod(0o750)

        for file_name in ["scan_data.php", "auth.php"]:
            file_path = data_dir / file_name
            if file_path.exists():
                # Set permissions based on hosting panel only if not already correct
                current_file_mode = file_path.stat().st_mode & 0o777
                if current_file_mode != file_permissions:
                    file_path.chmod(file_permissions)

        return True
    except Exception as error:
        logger.error(
            "Failed to fix permissions for site=%s error=%s",
            site,
            error,
        )
        return False


async def fix_data_file_permissions_everywhere(sink):
    """
    Fix data file permissions for all WordPress sites with imunify-security plugin installed.

    Args:
        sink: The telemetry/event sink
    """
    fixed = set()
    failed = set()

    with inactivity.track.task("wp-plugin-fix-permissions"):
        try:
            clear_caches()

            # Get all installed sites
            installed_sites = get_installed_sites()
            if not installed_sites:
                return

            # Determine file permissions based on hosting panel
            from defence360agent.subsys.panels.hosting_panel import (
                HostingPanel,
            )
            from defence360agent.subsys.panels.plesk import Plesk

            file_permissions = (
                0o440 if HostingPanel().NAME == Plesk.NAME else 0o400
            )

            # Process sites
            for site in installed_sites:
                if await remove_site_if_missing(sink, site):
                    continue

                success = await fix_site_data_file_permissions(
                    site, file_permissions
                )
                if success:
                    fixed.add(site)
                else:
                    failed.add(site)

            logger.info(
                "Fixed data file permissions for %d WordPress sites, %d"
                " failed",
                len(fixed),
                len(failed),
            )

        except asyncio.CancelledError:
            logger.info(
                "Fixing data file permissions was cancelled. Permissions were"
                " fixed for %d sites",
                len(fixed),
            )
        except Exception as error:
            logger.error(
                "Error occurred during permission fixing. error=%s", error
            )