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/dedrads
Viewing File: /opt/dedrads/cms_counter.py
#!/usr/lib/rads/venv/bin/python3 """ CMS Counter 2.4 01/05/2026 Corey S ------- All Server Version - Should be acceptable for any server environment - Server Type determined by Fabric wrapper ckx.run_cms_counter() and passed to script via '-s' - 3rd Party/Additional Panels added to determine_panel_type() - Refactored NodeJS detection split into its own module - '--new' switch added to only check newly provisioned accounts - Overall code refactored to be more efficient - Determine and print version of certain CMS """ import os import subprocess import sys from pathlib import Path import argparse import json import re import socket import rads def run(command=None, check_flag=True, error_flag=True): """ check_flag ignores if the command exits w/ anything other than 0 if True error_flag sends errors to /dev/null if set to True """ output = None if command and str(command) and error_flag is True: try: output = ( subprocess.run( command, shell=True, check=check_flag, stdout=subprocess.PIPE, ) .stdout.decode("utf-8") .strip() ) except Exception as e: print(e) elif command and str(command) and error_flag is False: try: output = ( subprocess.run( command, shell=True, check=check_flag, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, ) .stdout.decode("utf-8") .strip() ) except Exception: return None return output def determine_panel_type(): """ Determines type of Panel in use on server, i.e. CWP, Platform i, Plesk, etc Panels Currently Supported: - cPanel - CWP - Platform i - Webmin - Plesk - ISPConfig - DirectAdmin - CloudPanel """ if os.path.exists('/var/cpanel'): # cPanel return 'cPanel' if os.path.exists('/usr/local/cwp'): # CWP return 'CWP' if os.path.exists('/etc/profile.d/wp3.sh'): # Platform i return 'Platform_i' if os.path.exists('/etc/webmin'): # Webmin return 'Webmin' if os.path.exists( '/etc/httpd/conf.d/zz010_psa_httpd.conf' ) or os.path.exists( '/etc/apache2/conf.d/zz010_psa_httpd.conf' ): # Plesk return 'Plesk' if os.path.exists('/usr/local/directadmin'): # DirectAdmin return 'DirectAdmin' if os.path.exists('/usr/local/bin/dploy'): # CloudPanel return 'CloudPanel' if os.path.exists( '/home/admispconfig/ispconfig/lib/config.inc.php' ): # ISPConfig 2 return 'ISPConfig-2' if os.path.exists( '/usr/local/ispconfig/interface/lib/config.inc.php' ): # ISPConfig 3 return 'ISPConfig-3' return 'cVPS' def cms_version(docroot=None, cms=None): """ Determine the CMS version if possible Currently supported CMS: - Wordpress - Moodle - Joomla """ version_cmd = None if cms == 'Wordpress': if os.path.exists(f"""{docroot}/wp-includes/version.php"""): version_cmd = f"""grep -E 'wp_version =' {docroot}/wp-includes/version.php | """ + """awk '{print $3}' | tr -d ';'""" elif cms == 'Joomla': if os.path.exists(f"""{docroot}/libraries/cms/version/version.php"""): version_cmd = r"""grep -E 'public \$RELEASE' """ + f"""{docroot}/libraries/cms/version/version.php | """ + """awk'{print $4}' | tr -d ';'""" elif os.path.exists(f"""{docroot}/libraries/src/Version.php"""): version_cmd = f"""grep -E 'const RELEASE' {docroot}/libraries/src/Version.php | """ + """awk '{print $4}' | tr -d ';'""" elif cms == 'Moodle': if os.path.exists(f"""{docroot}/version.php"""): version_cmd = r"""grep -E '\$release =' """ + f"""{docroot}/version.php | """ + """awk '{print $3" "$4" "$5}' | tr -d ';'""" if version_cmd: try: version = run(version_cmd) except: version = None else: version = None return version def identify_apache_config(): """Function to determine Apache config file to use to check domains as different installs/versions can store the config in different files, starting w/config for 2.2 and checking different 2.4 config locations to end""" if os.path.exists('/usr/local/apache/conf/httpd.conf'): return '/usr/local/apache/conf/httpd.conf' if os.path.exists('/etc/httpd/conf/httpd.conf'): return '/etc/httpd/conf/httpd.conf' if os.path.exists('/etc/apache2/httpd.conf'): return '/etc/apache2/httpd.conf' if os.path.exists('/etc/apache2/conf/httpd.conf'): return '/etc/apache2/conf/httpd.conf' sys.exit('Unable to determine Apache config!\nQuitting...') def identify_nginx_config(): """ Function to find NGINX config file """ if os.path.exists('/etc/nginx/vhosts'): return '/etc/nginx/vhosts' if os.path.exists('/etc/nginx/conf.d/vhosts'): return '/etc/nginx/conf.d/vhosts' if os.path.exists('/etc/nginx/nginx.conf'): return '/etc/nginx/nginx.conf' sys.exit("Unable to locate NGINX config file! Quitting...") def find_nodejs(docroot=None, domain=None): """ Find possible Node.JS installs per doc root """ install_list = [] if docroot and '\n' not in str(docroot): user = docroot.split('/')[2] if os.path.exists(f"""{docroot}/.htaccess"""): try: with open( f"""{docroot}/.htaccess""", encoding='utf-8' ) as htaccess: for line in htaccess.readlines(): if 'PassengerAppRoot' in line: install_dir = line.split()[1].strip().strip('"') if ( f"""{domain}:{install_dir}""" not in install_list ): # Dont want a dictionary as a single domain # could have multiple subdir installs install_list.append( f"""{domain}:{install_dir}""" ) except Exception: return None if ( len(install_list) == 0 ): # If not found in htaccess, check via procs instead user_id = run( f"""id -u {user}""", check_flag=False, error_flag=False ) if user_id and user_id.isdigit(): # Only return procs whose true owner is the user ID of the # currently checked user node_procs = run( f"""pgrep -U {user_id} node""", check_flag=False, error_flag=False, ).split('\n') if len(node_procs) > 0: for pid in node_procs: try: cwd = run( f"""pwdx {pid} | cut -d ':' -f 2""", check_flag=False, error_flag=False, ) command = run( f"""ps --no-headers -o command {pid} | """ + """awk '{print $2}'""", check_flag=False, error_flag=False, ).split('.')[1] install_dir = cwd + command except Exception: return None if ( install_dir and os.path.exists(install_dir) and f"""{domain}:{install_dir}""" not in install_list ): install_list.append(f"""{domain}:{install_dir}""") return install_list def determine_cms(docroot=None): """Determine CMS manually with provided document root by matching expected config files for known CMS""" docroot = str(docroot) cms_dictionary = { f"""{docroot}/concrete.php""": 'Concrete', f"""{docroot}/Mage.php""": 'Magento', f"""{docroot}/clientarea.php""": 'WHMCS', f"""{docroot}/configuration.php""": 'Joomla', f"""{docroot}/ut.php""": 'PHPList', f"""{docroot}/passenger_wsgi.py""": 'Django', f"""{docroot}/wp-content/plugins/boldgrid-inspirations""": 'Boldgrid', f"""{docroot}/wp-config.php""": 'Wordpress', f"""{docroot}/sites/default/settings.php""": 'Drupal', f"""{docroot}/includes/configure.php""": 'ZenCart', f"""{docroot}/config/config.inc.php""": 'Prestashop', f"""{docroot}/config/settings.inc.php""": 'Prestashop', f"""{docroot}/app/etc/env.php""": 'Magento', f"""{docroot}/app/etc/local.xml""": 'Magento', f"""{docroot}/vendor/laravel""": 'Laravel', } for config_file, content in cms_dictionary.items(): if os.path.exists(config_file): return content if os.path.exists(f"""{docroot}/index.php"""): try: with open(f"""{docroot}/index.php""", encoding='utf-8') as index: for line in index.readlines(): if re.search('CodeIgniter', line, re.IGNORECASE): return 'CodeIgniter' except Exception as e: print(e) if os.path.exists(f"""{docroot}/main.ts"""): return 'Angular' if os.path.exists(f"""{docroot}/main.js"""): if os.path.exists(f"""{docroot}/index.php"""): try: with open( f"""{docroot}/index.php""", encoding='utf-8' ) as index: for line in index.readlines(): if re.search('bootstrap', line, re.IGNORECASE): return 'Bootstrap' return 'Javascript' except Exception as e: print(e) else: return 'Javascript' if os.path.exists(f"""{docroot}/config.php"""): if os.path.exists(f"""{docroot}/admin/config.php"""): return 'OpenCart' try: with open(f"""{docroot}/config.php""", encoding='utf-8') as config: for line in config.readlines(): if 'Moodle' in line: return 'Moodle' if os.path.exists(f"""{docroot}/cron.php"""): with open( f"""{docroot}/cron.php""", encoding='utf-8' ) as cron: for line in cron.readlines(): if 'phpBB' in line: return 'phpBB' return 'Undetermined' except Exception as e: print(e) if os.path.exists(f"""{docroot}/index.html"""): return 'HTML' return None def double_check_wordpress(directory=None): if directory and os.path.exists(directory): if os.path.exists( f"""{directory}/wp-content/plugins/boldgrid-inspirations""" ): return 'Boldgrid' return 'Wordpress' def manual_find_docroot(domain=None, web_server_config=None, web_server=None): docroot_cmd = None if web_server == 'apache': docroot_cmd = ( f"grep 'ServerName {domain}' {web_server_config} -A3 | " "grep DocumentRoot | " + r"""awk '{print $2}' | uniq""" ) domain_name = domain elif web_server == 'nginx': domain_name = domain.removesuffix('.conf') if r'*' in domain_name: return None # Skip wildcard subdomain configs if domain_name.count('_') > 0: new_domain = '' if domain_name.split('_')[1] == '': # user__domain_tld.conf domain_name = domain_name.split('_') limit = len(domain_name) - 1 start = 2 while start <= limit: new_domain += domain_name[start] if start != limit: new_domain += '.' start += 1 domain_name = new_domain else: # domain_tld.conf limit = len(domain_name) - 1 start = 0 while start <= limit: new_domain += domain_name[start] if start != limit: new_domain += '.' start += 1 domain_name = new_domain # This is the file name, above we extracted the actual domain # for use later nginx_config = f"{web_server_config}/{domain}" if os.path.exists(nginx_config): docroot_cmd = ( f"""grep root {nginx_config} | """ + r"""awk '{print $2}' | uniq | tr -d ';'""" ) else: docroot_cmd = None if docroot_cmd: docroot = run(docroot_cmd) else: print(f"""Cannot determine docroot for: {domain_name}""") return None return docroot def cms_counter_no_cpanel( verbose=False, server=None, user_list=None, unique_wordpress_users=None, unique_boldgrid_users=None, ): """ Function to get counts of CMS from all servers without cPanel """ if user_list is None: user_list = [] if unique_wordpress_users is None: unique_wordpress_users = [] if unique_boldgrid_users is None: unique_boldgrid_users = [] # Set Variables web_server_config = None domains_cmd = None domains_list = None domains = {} docroot_list = [] users = [] # List of system users not to run counter against - parsed from /etc/passwd sys_users = [ 'root', 'bin', 'daemon', 'adm', 'sync', 'shutdown', 'halt', 'mail', 'games', 'ftp', 'nobody', 'systemd-network', 'dbus', 'polkitd', 'rpc', 'tss', 'ntp', 'sshd', 'chrony', 'nscd', 'named', 'mailman', 'cpanel', 'cpanelcabcache', 'cpanellogin', 'cpaneleximfilter', 'cpaneleximscanner', 'cpanelroundcube', 'cpanelconnecttrack', 'cpanelanalytics', 'cpses', 'mysql', 'dovecot', 'dovenull', 'mailnull', 'cpanelphppgadmin', 'cpanelphpmyadmin', 'rpcuser', 'nfsnobody', '_imunify', 'wp-toolkit', 'redis', 'nginx', 'telegraf', 'sssd', 'scops', 'clamav', 'tier1adv', 'inmotion', 'hubhost', 'tier2s', 'lldpd', 'patchman', 'moveuser', 'postgres', 'cpanelsolr', 'saslauth', 'nagios', 'wazuh', 'systemd-bus-proxy', ] nginx_status = run( """systemctl status nginx 1>/dev/null 2>/dev/null;echo $?""" ) apache_status = run( """systemctl status httpd 1>/dev/null 2>/dev/null;echo $?""" ) # Determine Domain List # Even if NGiNX server generally Apache is still in use, and is much less # convoluted than the NGiNX config web_server = None if str(apache_status) == '0': web_server = 'apache' web_server_config = identify_apache_config() if web_server_config: domains_cmd = ( f"""grep ServerName {web_server_config} | """ + r"""awk '{print $2}' | sort -g | uniq""" ) # If Apache is not in use and NGiNX is, use NGiNX elif str(nginx_status) == '0': web_server = 'nginx' web_server_config = identify_nginx_config() if web_server_config: # THIS MAY NEED REFINED - is this compatible with all NGiNX configs # we're checking for? I think one doesn't end in .conf at least domains_cmd = ( f"find {web_server_config} -type f -name '*.conf' -print | " "grep -Ev '\\.ssl\\.conf' | xargs -d '\n' -l basename" ) if domains_cmd: domains_list = run(domains_cmd) # Get list of domains if domains_list: for domain_name in domains_list.split(): docroot = manual_find_docroot( domain=domain_name, web_server_config=web_server_config, web_server=web_server, ) if docroot and os.path.exists(docroot): node_installs = [] docroot_list.append(docroot) domains.update({f"""{docroot}""": f"""{domain_name}"""}) try: # Try and find NodeJS installs and add them to list of # user_installs node_installs += find_nodejs( docroot=docroot, domain=domain_name ) except Exception: pass # Check sub-directories bad_dirs = [ f"""{docroot}/wp-admin""", f"""{docroot}/wp-includes""", f"""{docroot}/wp-content""", f"""{docroot}/wp-json""", f"""{docroot}/admin""", f"""{docroot}/cache""", f"""{docroot}/temp""", f"""{docroot}/tmp""", f"""{docroot}/__wildcard__""", f"""{docroot}/cgi-bin""", f"""{docroot}/.well-known""", f"""{docroot}/config""", f"""{docroot}/language""", f"""{docroot}/plugins""", f"""{docroot}/media""", f"""{docroot}/libraries""", f"""{docroot}/images""", f"""{docroot}/includes""", f"""{docroot}/include""", f"""{docroot}/modules""", f"""{docroot}/components""", f"""{docroot}/templates""", f"""{docroot}/cli""", f"""{docroot}/layouts""", f"""{docroot}/storage""", f"""{docroot}/logs""", f"""{docroot}/bin""", f"""{docroot}/uploads""", f"""{docroot}/docs""", f"""{docroot}/style""", f"""{docroot}/files""", f"""{docroot}/sounds""", f"""{docroot}/cart""", f"""{docroot}/shop""", f"""{docroot}/assets""", f"""{docroot}/system""", f"""{docroot}/documentation""", f"""{docroot}/application""", f"""{docroot}/script""", f"""{docroot}/scripts""", f"""{docroot}/backups""", f"""{docroot}/backup""", ] docroot_dir = Path(docroot) try: for d in docroot_dir.iterdir(): if ( os.path.exists(d) and d.is_dir() and str(d) not in bad_dirs ): try: node_installs += find_nodejs( docroot=str(d), domain=domain_name ) except Exception: pass dirname = str(os.path.basename(d)) d = str(d) # Convert from Path object to String docroot_list.append(d) domains.update( {f"""{d}""": f"""{domain_name}/{dirname}"""} ) bad_dirs.append(d) except NotADirectoryError: continue except Exception as e: print(e) continue # Determine User List if len(user_list) >= 1: users = user_list # Get users if they weren't passed already - if cPanel was detected but not # Softaculous for instance if os.path.exists('/home') and len(user_list) == 0: users_dir = Path('/home') try: with open('/etc/passwd', encoding='utf-8') as passwd: passwd_file = passwd.readlines() except Exception: sys.exit('Unable to read /etc/passwd!\nExiting...') for u in users_dir.iterdir(): if u.is_dir(): limit = len(u.parts) - 1 for line in passwd_file: if ( u.parts[limit] == line.split(':')[0] and u.parts[limit] not in sys_users ): users.append(u.parts[limit]) if len(users) >= 1 and len(docroot_list) >= 1: for docroot in docroot_list: get_cms = None docroot_user = None for user in users: if user in sys_users: continue # Go to next user if current user is System user if user in docroot.split('/'): docroot_user = user break get_cms = determine_cms(docroot=docroot) if ( str(get_cms).lower() == 'wordpress' and docroot_user and docroot_user not in unique_wordpress_users ): unique_wordpress_users.append(docroot_user) elif ( str(get_cms).lower() == 'boldgrid' and docroot_user and docroot_user not in unique_boldgrid_users ): unique_boldgrid_users.append(docroot_user) if verbose: pt = determine_panel_type() # Determine Panel Type if needed version = cms_version(docroot=docroot,cms=get_cms) if get_cms and docroot_user and docroot_user not in sys_users: domain = domains.get(f"""{docroot}""", None) hostname = socket.gethostname().split('.')[0] if os.path.exists(f"/var/cpanel/users/{docroot_user}"): plan_type = run( f"""grep PLAN /var/cpanel/users/{docroot_user}""" ).split('=')[1] else: plan_type = 'N/A' if verbose: print( f"{hostname} {server} {pt} {docroot_user} " f"{plan_type} {domain} {get_cms} {version}" ) else: print(f"""{docroot_user} {domain} {get_cms}""") for install in node_installs: if verbose: print( f"{hostname} {server} {pt} {docroot_user} " f"{plan_type} {install} NodeJS" ) else: print(f"""{docroot_user} {install} NodeJS""") print(f"""Unique Wordpress Users: {len(unique_wordpress_users)}""") print(f"""Unique Boldgrid Users: {len(unique_boldgrid_users)}""") def cms_counter_cpanel(verbose=False, new=False, server=None): """ Function to determine CMS counter on servers with cPanel Attempting to use Softaculous first Checking manually if that returns no results Including users list as passed variable so we can run against specific users only as well, future-proofing/anticipating possible request """ # Establish Variables softaculous = 0 unique_wordpress_users = [] unique_boldgrid_users = [] # Softaculous omits the Name of some CMS in table view for some awesome # reason; this is a fallback dictionary to use to manually retrieve Name # if it isnt included. # Moodle and Magento only ones so far I've determined with this issue softaculous_ids = { '503': 'Moodle 3.5', '542': 'Moodle', '98': 'Moodle 2.6', '343': 'Moodle 2.0', '517': 'Moodle 3.7', '485': 'Moodle 3.6', '670': 'Moodle 3.9', '668': 'Moodle 3.8', '699': 'Moodle 3.11', '681': 'Moodle 3.10', '706': 'Moodle 4.0', '713': 'Moodle 4.1', '67': 'Magento 1.9', '545': 'Magento 2.3', '465': 'Magento 1.7', '486': 'Magento 2.2', '665': 'Magento', '688': 'Magento 2.4.2', '709': 'Magnto 2.4.1', } cms_info = {} site_owners = {} user_list = [] no_install_users = [] # List of system users not to run counter against - parsed from /etc/passwd sys_users = [ 'root', 'bin', 'daemon', 'adm', 'sync', 'shutdown', 'halt', 'mail', 'games', 'ftp', 'nobody', 'systemd-network', 'dbus', 'polkitd', 'rpc', 'tss', 'ntp', 'sshd', 'chrony', 'nscd', 'named', 'mailman', 'cpanel', 'cpanelcabcache', 'cpanellogin', 'cpaneleximfilter', 'cpaneleximscanner', 'cpanelroundcube', 'cpanelconnecttrack', 'cpanelanalytics', 'cpses', 'mysql', 'dovecot', 'dovenull', 'mailnull', 'cpanelphppgadmin', 'cpanelphpmyadmin', 'rpcuser', 'nfsnobody', '_imunify', 'wp-toolkit', 'redis', 'nginx', 'telegraf', 'sssd', 'scops', 'clamav', 'tier1adv', 'inmotion', 'hubhost', 'tier2s', 'lldpd', 'patchman', 'moveuser', 'postgres', 'cpanelsolr', 'saslauth', 'nagios', 'wazuh', 'systemd-bus-proxy', ] if secure_user := rads.SECURE_USER: sys_users.append(secure_user) if os.path.exists('/var/cpanel/users'): if new: user_list_cmd = """tail -250 /etc/passwd | cut -d ':' -f 1""" user_list = run(user_list_cmd).split('\n') else: users_dir = Path('/var/cpanel/users') for u in users_dir.iterdir(): limit = len(u.parts) - 1 if ( not u.is_dir() and u.parts[limit] != 'system' and '.bak' not in u.parts[limit] ): user_list.append(u.parts[limit]) else: sys.exit('Unable To Determine cPanel Users!\nQuitting...') if len(user_list) == 0: # Confirm Users sys.exit('No Users Detected. Quitting...') if not os.path.exists('/var/cpanel/users'): # Confirm cPanel sys.exit('cPanel not detected. Quitting...') else: if os.path.exists('/var/softaculous'): # Confirm Softaculous softaculous = 1 if softaculous == 1: for user in user_list: if user in sys_users: continue # Go to next user if system user detected plan_type = run(f"""grep PLAN /var/cpanel/users/{user}""").split( '=' )[1] user_installs = None user_installs_json = None node_installs = [] cmd = ( "/usr/local/cpanel/3rdparty/bin/php " "/usr/local/cpanel/whostmgr/docroot/cgi/softaculous/cli.php " f"--list_ins --user={user} --resp=json" ) user_installs = run(cmd, check_flag=False) try: user_installs_json = json.loads(user_installs) except Exception: user_installs_json = None if user_installs_json and 'error' in user_installs_json.keys(): no_install_users.append(user) continue # Jump to next user if user_installs_json: install_list = [ install for install in user_installs_json if install != 'error' ] for install in install_list: try: user_cms = user_installs_json[install]['script_name'] url = user_installs_json[install]['softurl'] version = user_installs_json[install]['ver'] if ( not user_cms ): # some scripts will give 'null' as the script_name try: sid = user_installs_json[install][ 'sid' ] # Get script id user_cms = softaculous_ids[ sid ] # determine CMS from script id except Exception: print(f"""{user}: Error determining CMS""") continue if user_cms and url: # Softaculous only reports Wordpress, check if its # really Boldgrid before recording it if user_cms == 'WordPress': docroot = user_installs_json[install][ 'softpath' ] user_cms = double_check_wordpress( directory=docroot ) if ( str(user_cms).lower() == 'wordpress' and user and user not in unique_wordpress_users ): unique_wordpress_users.append(user) elif ( str(user_cms).lower() == 'boldgrid' and user and user not in unique_wordpress_users ): unique_boldgrid_users.append(user) user_cms = user_cms + f' {version}' cms_info.update({url: user_cms}) site_owners.update({url: user}) except Exception as e: print(e) continue else: no_install_users.append(user) continue # Check for NodeJS installs apache_status = run( """systemctl status httpd &>/dev/null;echo $?""", check_flag=False, ) nginx_status = run( """systemctl status nginx &>/dev/null;echo $?""", check_flag=False, ) get_domains_cmd = f"""grep -E '{user}$' /etc/userdomains""" domain_list = run(get_domains_cmd, check_flag=False).split( '\n' ) # .split(':')[0].strip() # This ensures the actual correct user is used and not one # containing the same string for domain_string in domain_list: if ( len(domain_string.split(':')) > 1 and str(domain_string.split(':')[1]).strip() == str(user).strip() ): domain = domain_string.split(':')[0] if str(apache_status) == '0': web_server_config = identify_apache_config() web_server = 'apache' elif str(nginx_status) == '0': web_server_config = identify_nginx_config() web_server = 'nginx' else: print('Unable to identify web server config') return docroot = manual_find_docroot( domain=domain, web_server_config=web_server_config, web_server=web_server, ) if web_server_config and docroot: try: node_installs += find_nodejs( docroot=docroot, domain=domain_string.split(':')[0].strip(), ) except Exception: pass if len(node_installs) > 0: for install in node_installs: # Add found NodeJS installs to dictionaries for each user, # allowing reporting to print them at the end cms_info.update({install.split(':')[1]: 'NodeJS'}) site_owners.update({install.split(':')[1]: user}) if verbose: hostname = socket.gethostname().split('.')[0] for url, cms in cms_info.items(): site_owner = site_owners.get(url, None) print( f"{hostname} {server} cPanel {site_owner} " f"{plan_type} {url} {cms}" ) else: for url, cms in cms_info.items(): site_owner = site_owners.get(url, None) print(f"""{site_owner} {url} {cms}""") else: # cPanel present but no Softaculous try: cms_counter_no_cpanel( verbose=verbose, server=server, unique_wordpress_users=unique_wordpress_users, unique_boldgrid_users=unique_boldgrid_users, ) except Exception as e: print(e) if ( len(no_install_users) >= 1 ): # Run no softaculous cms_counter for users w/o installs found cms_counter_no_cpanel( user_list=no_install_users, verbose=verbose, server=server, unique_wordpress_users=unique_wordpress_users, unique_boldgrid_users=unique_boldgrid_users, ) else: print(f"""Unique Wordpress Users: {len(unique_wordpress_users)}""") print(f"""Unique Boldgrid Users: {len(unique_boldgrid_users)}""") return def main(): """Main function, initializes global variables as globals, gets hostname and call relevant checks after determining enviroment""" parser = argparse.ArgumentParser(description='CMS Counter 2.1') parser.add_argument( '-v', '--verbose', dest='verbose', help='Include Panel Type and Server Type in output', action='store_const', const=True, default=False, ) parser.add_argument( '-n', '--new', dest='new', help='Check the 250 most recently provisioned users only', action='store_const', const=True, default=False, ) parser.add_argument( '-s', '--server-type', dest='server', help='Server Type - passed by Fabric generally', action='store', ) args = vars(parser.parse_args()) server_types = ['Shared', 'Reseller', 'Hub', 'VPS', 'Dedicated'] server_type = None for st in server_types: if ( args['server'] == st.lower() or args['server'] == st or args['server'] == st.upper() ): server_type = st break if not server_type: sys.exit('Server Type not given!\nQuitting...') verb = args['verbose'] if os.path.exists('/var/cpanel/users'): cms_counter_cpanel(verbose=verb, server=server_type) else: cms_counter_no_cpanel(verbose=verb, server=server_type) if __name__ == "__main__": main()