hrldZddlZddlZddlZddlZddlZddlmZddlm Z ddl m Z ddl m Z ddlmZmZmZmZddlmZdd lmZdd lmZmZdd lmZdd lmZdd lm Z ddl!m"Z"ddl#m$Z$m%Z%m&Z&ddl'm(Z(ddl)m*Z*m+Z+m,Z,m-Z-ddl.m/Z/ddl0m1Z1ddl2m3Z3m4Z4m5Z5ddl6m7Z7m8Z8m9Z9ddl:m;Z;ddlZ>m?Z?ddl@mAZAmBZBddlCmDZDddlEmFZFddlGmHZHmIZIddlJmKZKddlLmMZMmNZNddlOmPZPddlQmRZRmSZSe eTZUd ZVe/d!e*jWZXe eBjYd"#ZZe eBjYd$#Z[e5e3eUj\%eUj]Z^d&eeBd'efd(Z_Gd)d*e$e%Z`Gd+d,e$e%ZaGd-d.e$Zbd/ZcGd0d1eaZdGd2d3e$ZeGd4d5e$ZfdS)6u  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 . Copyright © 2019 Cloud Linux Software Inc. This software is also available under ImunifyAV commercial license, see N)suppress)partial) getLogger)Path)DictIterableListTuple)utils) inactivity)MalwareMyImunifyConfig) HookEvent) LicenseCLN) MessageType)myimunify_protection_enabled) MessageSink MessageSourceexpect)g)Scope nice_iteratorrecurring_checksplit_for_chunk)register_lock_file) check_lock)DAYMINUTE rate_limit) CleanupResultMalwareCleanerMalwareCleanupProxy)CleanupStorage)MalwareHitStatusMalwareScanResourceTypeMalwareScanType)MalwareHistory MalwareHit)ScanAlreadyCompleteError)MalwareDatabaseCleaner)MDSDetachedCleanupMDSDetachedRestore)MalwareDatabaseRestore)HackerTrapHitsSaver MalwareAction)malware_response)get_username_by_uidis_uidcleanupstatus) attributeowner)periodon_drophitsreturncd|DS)Nc3DK|]}|jtjk|VdSN)r5r$FOUND.0hits T/opt/imunify360/venv/lib/python3.11/site-packages/imav/malwarelib/plugins/cleanup.py z#filter_cleanable..bs2 H HC3:1A1G#G#GC#G#G#G#G H Hr:s rCfilter_cleanablerHas H H4 H H HHrEc VeZdZdZdZdZdZeej de fdZ e de deed ee e ffd Ze de d ed ed e fd Zde d e fdZde d e fdZdZdZe dZdZdZ ddZdZeddZdS)Cleanupchd|_d|_d|_d|_d|_d|_d|_dS)NF) _cleanup_task_store_original_task_running_loop_sink_proxy_cleanerselfs rC__init__zCleanup.__init__fs9!$(!     rEcK||_||_t|_t |||_|||_dS)N)loopsink) rOrPr"rQr!rR create_taskr4rLrTrWrXs rC create_sourcezCleanup.create_sourceosY  )++ &Dt<<< !--dllnn==rEc KdSr>rFrTrWs rC create_sinkzCleanup.create_sinkv  rEcK|jrZ|jttj5|jd{VddddS#1swxYwYdSdSr>)rLcancelrasyncioCancelledErrorrSs rCshutdownzCleanup.shutdownys   )   % % ' ' ''011 ) )(((((((( ) ) ) ) ) ) ) ) ) ) ) ) ) ) ) ) ) ) ) )AAAmessagec K|d}|d}|d}|d}|d}|du}t|d}tj|d| } t | } d| D} |sh|| t jt jg\} } | | d{V} | | | d{Vz} |t| z x} rt d | ||j || ||||||_dS) Ncause initiator post_actionscan_id standard_onlyr:)include_scan_infocFg|]}|jtjjk|SrF) resource_typer%FILEvaluer@s rC z0Cleanup.process_cleanup_task..s7    $;$@$FFF FFFrEz"%s/%s hits filtered before cleanup)getlenr( refresh_hitsrH_split_hits_by_scan_typer&RESCANRESCAN_OUTDATED_filter_rescan_hits_filter_failed_to_cleanup_hitsloggerinforOrY_store_originalrM) rTrfrhrirjrkrlmanual_cleanuporigin_hits_numr: rescan_hitsfiltereds rCprocess_cleanup_taskzCleanup.process_cleanup_tasks G$$KK ,, kk-00 ++i(( O44 $gfo..& FO>/A    %%       !% = =-/NO!! K!% 8 8 E EEEEEEEKt'J'J((""""""D'T2 28  KK4    %)J$:$:  eY Wm  % % !!!rEr: scan_typesr;cgg}}|D];}|jj|vr||&||<||fSr>)scanidtypeappend)r:r target_hits other_hitsrBs rCrvz Cleanup._split_hits_by_scan_typesd#%bZ  ' 'Cz*,,""3''''!!#&&&&J&&rE time_rangeallowed_attemptsc Kg}|rtj|z }i}t|dD]\}|tt jd|D|t jdd{V]|D]P}||j d}||krtd|j ||;| |Q|S)N chunk_sizecg|] }|j SrF orig_filer@s rCrrz5Cleanup._filter_failed_to_cleanup..sAAAsS]AAArE)sincerzVSkip cleanup file '%s', since there are too many attempts to cleanup it in %s sec [%s]) timerupdatedictr'get_failed_cleanup_events_countrbsleeprsrthrottled_log_errorr) r:rr hits_to_cleanrfailed_cleanup_count hits_chunkrBfailuress rC_filter_failed_to_cleanupz!Cleanup._filter_failed_to_cleanups3  *IKK*,E#% -dsCCC ' ' $++&FAAjAAA"'mA&&&&&&&&&& * */33CM1EE///'@ " $$S))))rEcRK||dtzdd{VS)Nrr)rrrTr:s rCryzCleanup._filter_rescan_hitssL33 QZ!4         rEcVK||ttd{VS)zl Don't try to cleanup the same hit more than *COUNT_OF_ATTEMPTS_TO_CLEANUP_PER_DAY* rN)rr$COUNT_OF_ATTEMPTS_TO_CLEANUP_PER_DAYrs rCrzz&Cleanup._filter_failed_to_cleanup_hitssL 33 A4         rEc  Ktj|tjt |}t jd5tj |d{V\}} } dddn #1swxYwY| D]w} |j tj d| jtjt#t%jd{Vx||||||||D]&\} tj fd| D| 'tj| t-j| ||d{VdS)Ncleanup_storagez*Failed to store the original from {} to {})rf timestampcg|]}|v| SrFrF)rAhhit_lists rCrrz+Cleanup._store_original..s"F"F"FX 1 rErhri)r( set_statusr$CLEANUP_STARTED_group_by_statusr tracktaskr# store_allrPprocess_messager CleanupFailedformatrpathintr _add_to_proxyitemsdelete_instancesr/cleanup_failed)rTr:rhrirjrkrloriginal_status succeededfailed not_existrBr5rs @rCr}zCleanup._store_originals5 d$4$DEEE*400   " "#4 5 5 P P1?1I$1O1O+O+O+O+O+O+O (Ivy P P P P P P P P P P P P P P P  C*,,)DKKM>+>"$)++..             uig}   !0 5 5 7 7 P P FH  !"F"F"F"Ff"F"F"F O O O O#I...* Ui            sA<<BBcg}g}|D]?} t||} | r|| *|| @|j||||d||j||||||dS)NT)"decide_if_standard_signatures_onlyrrQadd) rTr:rhrirjrkrlstandard_only_hits advanced_hitsrBstandard_only_users rCrzCleanup._add_to_proxys   * *C!C="" " *"))#....$$S))))                        rEc$t|}|Sr>)_group_by_user)r: user_hitss rC _user_hitszCleanup._user_hits#s"4(( rEc# K|j}|D].\}}}}}}d|D fd|D}| |||||fV/dS)NcFg|]}tjd|j|S)z\w+-BLKH-|cloudhash\.|cld-)rematchrr@s rCrrz0Cleanup._cloud_assisted_hits..3s=8938DDrEcg|]}|v| SrFrF)rArB blacklists rCrrz0Cleanup._cloud_assisted_hits..8s#LLLCs)7K7KC7K7K7KrE)rQflush) rT action_hitsrhrirjrkrlall_hits regular_hitsrs @rC_cloud_assisted_hitszCleanup._cloud_assisted_hits(sk''))         #I MLLL8LLLL      rEcKd|D}tj||t||}|j|d{VdS)Nc6g|]}|SrF)as_dictr@s rCrrz'Cleanup._start_hook..Ds ...# ...rE) cleanup_idstarted total_filesDUMP)rMalwareCleanupStartedrtrPr)rTrrr:dumpcleanup_starteds rC _start_hookzCleanup._start_hookCsr.....#9!D      j((99999999999rENc&K||}||pg} h|| D]} || g} | | g} | | z} d| D}d| D}td||zt jj}tj}t| r| }tj s\t d|d|j tj| i||d||||g d{Vt!|d{Vx}s td|d L|} |||| d{V|j| |t*j|| d{V\}}}|j tj| ||||||||| d{VdS) Ncg|] }|j SrFrr@s rCrrz(Cleanup._clean_files.._s;;;sS];;;rEcg|] }|j SrFrr@s rCrrz(Cleanup._clean_files..`s999sS]999rEzCleaning files: %sz)Can't clean files for non panel user uid=z, since license is limitedz#Cleanup failed. License restriction) r:resultrrerrorrhrirjrkargszCan't find username for uid=z. Skip cleanup)softrrl)rrsr{debuguuiduuid4hexrr2r is_unlimitedrrPrrMalwareCleanupr1warningrrRstartConfig CLEANUP_TRIM)rTr:rrhrirjrkrlruser_hits_blackuser hits_regular hits_black user_hits_allfilesblackrruidusernamerrcmds rC _clean_fileszCleanup._clean_filesMsOOD)) //)/r::2i2/2< < D$==r22L(,,T266J(:5M;;l;;;E99j999E LL-uu} = = =)JikkGd|| !.00LL3333*44#2!.#%'1$+"G"'&/(3$+!#            *=c*B*B$B$B$B$B$B$BBNNG3GGG"":w FF F F F F F F F'+}':':(+ (;(("""""" FE3*,,*&!)#' +#             _< < rEc K|jrdS|jjs|jdSd|_tjd5 |}|D],\}}}}}}}||||||||d{V- d|_n #d|_wxYw ddddS#1swxYwYdS)NTr4)rrhrirjrkrlF) rNrQr:resetr rrrr) rTdatarrrhrirjrkrls rC_cleanupzCleanup._cleanupsv =  F{  K      F   " "9 - - & & &0022!++ "+#"+$/ '&3,&!&  %%%% - & & & & & & & & & & & & & & & & & &s+CAB'C' B00CCCc>K|d{VdSr>)rrSs rCr4zCleanup.cleanups,mmoorE)NNNNNN) __name__ __module__ __qualname__rUr[r^rdrrMalwareCleanupTaskrr staticmethodlistr r&r rvfloatrrryrzr}rrrrrrrr4rFrErCrJrJes >>>   )))  VK *++( $( ( ( ,+( T ' ' $_ 5 ' tTz  ' ' '\ '#( d t       $        <   >\6:::IIIIV&&&B_QrErJceZdZejZdZdZed de e de fdZ e ejdZdZdS) ResultProcessorc KdSr>rFr]s rCr^zResultProcessor.create_sinkr_rEcK||_dSr>)rPrZs rCr[zResultProcessor.create_sources rENr:r5cXtj||||D]}||_||_dSr>)r(rr5 cleaned_at)r:r5rrBs rC_set_hit_statuszResultProcessor._set_hit_statuss@dFJ777 ( (CCJ'CNN ( (rEcK|d}|d|d}|d}tj}fd|D}fd|D}g}t|d23d{V} | rD| jr|| P|| f6tj||| d{Vfd |D} tj | || d{V| | tj |fd |D} tj | || d{Vfd |D} tj| || d{V| | tj|fd |D} tj| || d{V| | tj|t%j|t)|| D]\}}| |||| d{V|S)Nr:rrhricg|]}|v| SrFrFrArBrs rCrrz0ResultProcessor.store_result..s:::SC6MMSMMMrEcg|]}|v| SrFrFrs rCrrz0ResultProcessor.store_result..s#@@@sc.?.?s.?.?.?rEdrrcHg|]}||SrF)requires_myimunify_protectionrs rCrrz0ResultProcessor.store_result..s?) ) ) c{88::) ) ) ) rEcHg|]}||SrF) is_failedrs rCrrz0ResultProcessor.store_result..s.FFF#fSk.C.C.E.EF#FFFrEcHg|]}||SrF) is_cleanedrs rCrrz0ResultProcessor.store_result...HHH3vc{/E/E/G/GH3HHHrEcHg|]}||SrF) is_removedrs rCrrz0ResultProcessor.store_result..rrE)rsrrrorig_file_pathexistsrr/cleanup_unable%cleanup_requires_myimunify_protectionrr$%CLEANUP_REQUIRES_MYIMUNIFY_PROTECTIONr cleanup_done CLEANUP_DONEcleanup_removedCLEANUP_REMOVEDr(rrr"send_failed_to_cleanup_hits_to_mrs)rTrfr:rhrinow processed unprocessedrrBrrcleanedremovedr5rrs @rC store_resultzResultProcessor.store_results!( ' 1 G$$KK ,, ikk::::D::: @@@@d@@@  &ySAAA * * * * * * *#c{$$&& *%,,..*&&s++++$$S)))B* u           ) ) ) )  ) ) ) % A ))            )  B    GFFFFFF* %9          IHHH)HHH( 5I           W&6&CSIIIHHHH)HHH+ 5I           W&6&FLLL#I... 0f E E K K M M 3 3 FH  6 2 2 2 255f=========sC&c K|r~|jtjd|Ddd{V|jtjd|Ddd{VdSdS)NcLg|]!}tj|j|j"SrF)r0HitInforhashr@s rCrrzFResultProcessor.send_failed_to_cleanup_hits_to_mrs..s9)0IIrEcleanup_failure_current)r: upload_reasonc g|];}tjttj||j.sR  )0 A# F FGGHrEcleanup_failure_original)rPrrMalwareMRSUpload)rTfailed_to_cleanup_hitss rCr'z2ResultProcessor.send_failed_to_cleanup_hits_to_mrss ! *,,,#9#<        *,,, $: #=                rEr>)rrrrAVSCOPEr^r[rr r(r5rrrrr-r'rFrErCr r s HE   ((d:.((((\(  VK &''==('=~rEr ceZdZdZdZdZdZdZee j e j dZ eedeed Zd S) StorageControllerz'Remove old backed up files from storagec6d|_tj|_dSr>) _clear_taskr CLEANUP_KEEP_keeprSs rCrUzStorageController.__init__*s( rEcbK|||_dSr>)rY daily_clearr?r]s rCr^zStorageController.create_sink.s.++D,<,<,>,>??rEcK|jrZ|jttj5|jd{VddddS#1swxYwYdSdSr>)r?rarrbrcrSs rCrdzStorageController.shutdown1s   '   # # % % %'011 ' '&&&&&&&& ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' 'rec|Ktj}||jtzz }||jdztzz }tjtj|ktj |d{V}|rt d|dSdS)Nrz/Cleanup storage have cleaned. Files removed: %s) rrArr(deletewhererexecuter#clearr{r|)rTr( keep_hits keep_origcleareds rC_clearzStorageController._clear7sikk$*s** 4:>S00 !!*"7)"CDDLLNNN&,Y77777777   KKA7       rEcK|jtjkr-tj|_|d{VdSdSr>)rArr@rM)rT_s rCconfig_updatedz StorageController.config_updatedBsP :, , ,,DJ++--          - ,rET)check_period_firstcheck_lock_period lock_filec>K|d{VdSr>)rMrSs rCrCzStorageController.daily_clearIs.kkmmrEN)rrr__doc__rUr^rdrMrr ConfigUpdater log_error_and_ignorerPrrr LOCK_FILErCrFrErCr=r='s11)))@@@'''    VK $%%U!!  "!&% _    rEr=cTtjsdS||dkst|r|SdS)z.isZ   U  "" ',&6&6&8&8 II   rEr)superr-rr.update_sa_hits)rTrf to_remove __class__s rCr-z!ResultProcessorIm360.store_resultfs,,W55555555  %h/5577   "0Y???????????rE) rrrrUrIM360r;rrrr- __classcell__)rds@rCr]r]_sl KE VK &''@@@@('@@@@@rEr]c^eZdZejZdZedZdZ dZ e e j dZe e jdZe e jde jfdZe e jd Ze e jde jfd Zd S) CleanupDbcd|_dSr>)rOrSs rCrUzCleanupDb.__init__ts  rEcKtjj}t|||d{VdSr>)rrrr*r)rapp_namers rC_start_cleanerzCleanupDb._start_cleanerwsJZ\\% $Zx@@FFHHHHHHHHHHHrEcKtjsWtjtjx} dSt d|j |j tj |gtj||j |j d{VdS)NzCleaning hit: (%s::%s))r(db_hits_under_cleanuprdb_hits_pending_cleanuporder_byrascfirstr{r|rrkrr$rrl)rTnext_hits rC _cleanup_nextzCleanupDb._cleanup_next|s  , . . 5 5 7 7 '>@@*.224455  F $h&8(:K    xj*:*JKKK!!("4h6GHHHHHHHHHHHrEcLK||_|d{VdSr>)rOrtr]s rCr^zCleanupDb.create_sinks7   """""""""""rEcKtj|d}t|}d|D}|sdStj|tj|d{VdS)Nr:cFg|]}|jtjjk|SrF)ror%DBrqr@s rCrrz2CleanupDb.process_cleanup_task..s7    $;$>$DDD DDDrE)r(rurHrr$CLEANUP_PENDINGrt)rTrfr:rdb_hitss rCrzCleanupDb.process_cleanup_tasks&wv77(..   $      Fg'7'GHHH  """""""""""rEcK|d}t|} |d{V}nT#t$rGtd|Yt jt|jddSwxYw t jt|jdn-#t jt|jdwxYwtj |d{VdS)Nrkz;Cannot complete cleanup %s, assuming it is already completeT ignore_errors) r+completer)r{rshutilrmtreer5 detached_dirrrXr)rTrfclean_iddetached_cleanupcleanup_outcomes rCparse_cleanup_resultszCleanupDb.parse_cleanup_resultssP9%-h77 $4$=$=$?$???????OO'    NNM     M$122$         M$122$     FM$122$     f$$_55555555555&6B4%BB4BB44*CrfcBKtj|j}tj|j}tj|t jtjtj|t j| d{VdSr>) r(db_hits_under_cleanup_inrrrr$r$rr?rt)rTrf cleaned_hits failed_hitss rCupdate_cleaned_hits_statusz$CleanupDb.update_cleaned_hits_statuss":7;LMM  9'.II  *7    k+;+ABBB  """""""""""rEcKtj}tj|tj|d{VdS)zc Clear the queue when the cleanup fails, set hits' status back to infected N)r(rnrr$r?rtrTrfr:s rCupdate_failed_hits_statusz#CleanupDb.update_failed_hits_statussW/11d$4$:;;;  """""""""""rEcKd}d}tj|j}tj|||d{Vtj|j}tj|||d{VdS)Nr)r( get_db_hitsrr/r#rr)rTrfrhrirrs rCsave_cleanup_events_in_historyz(CleanupDb.save_cleanup_events_in_historys !-g.?@@ (           !,W^<< * u             rEN)rrrrrer;rUrrlrtr^rrrrMalwareCleanCompleterMalwareDatabaseCleanuprMalwareDatabaseCleanupFailedrrrFrErCrhrhqsV KEII\IIII"### VK *++ # #,+ # VK ,--66.-6$ VK .// #"9 # # #0/ # VK 455 # #65 # VK .//  "9    0/    rErhcpeZdZejZdZedZdZ ede e de e fdZ e ejdZe ejdZe ejd Ze ejd Ze ejd Zd S) RestoreOriginalDbcd|_dSr>)rWrSs rCrUzRestoreOriginalDb.__init__s  rEcKtjsWtjtjx} dSt d|j |j t|j |j  d{Vtj|gtjdS)Nz$Restoring from cleanup hit: (%s::%s))rrk)r(db_hits_under_cleanup_restorerdb_hits_pending_cleanup_restorerprrqrrr{r|rrkr-restorerr$CLEANUP_RESTORE_STARTED)hit_to_restores rC _restore_nextzRestoreOriginalDb._restore_nexts   4 6 6 = = ? ? #-"L"N"N*.224455  F 2  $  #   %)N4K   '))          .F     rEcLK||_|d{VdSr>)rWrr]s rCr^zRestoreOriginalDb.create_sinks7   """""""""""rEr:r;cd|DS)Nc3DK|]}|jtjk|VdSr>)r5r$rr@s rCrDz:RestoreOriginalDb._filter_under_restore..s?  z-EEE EEEE  rErFrGs rC_filter_under_restorez'RestoreOriginalDb._filter_under_restores#      rEc6Ktjtjtj|jktj|jktj| d{VdSr>) r(rrzrGrrrkr$CLEANUP_RESTORE_PENDINGr)rTrfs rCqueue_db_restorez"RestoreOriginalDb.queue_db_restores   U:'7<7 8 8 U:&'*:: ; ;  4      """""""""""rEcK|d}t|} |d{V}nT#t$rGtd|Yt jt|jddSwxYw t jt|jdn-#t jt|jdwxYwtj |d{VdS)Nrkz;Cannot complete restore %s, assuming it is already completeTr|) r,r~r)r{rrrr5rrrXr)rTrf restore_iddetached_restorerestore_messages rCparse_restore_resultsz'RestoreOriginalDb.parse_restore_resultssPY' -j99 $4$=$=$?$???????OO'    NNM     M$122$         M$122$     FM$122$     f$$_55555555555rcKtj|j}tj||t j|d{VdSr>)r(rrrrr$r?r)rTrf restored_hitss rCupdate_restored_hits_statusz-RestoreOriginalDb.update_restored_hits_status&sq".w/@AA   & &} 5 57G7M     """""""""""rEc .K|d}|d}tj|j}|D]X}t j|j|jtj j |j |j |||j |j|j d{VYtj|j}|D]X}t j|j|jtj j |j |j |||j |j|j d{VYdS)Nrhri) rrkro file_owner file_userrirhdb_hostdb_portdb_name)rsr(rrr/cleanup_restored_originalrrkr%rxrqr7rrrrrcleanup_failed_restore)rTrfrhrirrBrs rCsave_restore_events_in_historyz0RestoreOriginalDb.save_restore_events_in_history.sW G$$KK ,, ".w/@AA    C 9]58>9(#              !,W^<<   C6]58>9(#                rEcKtj}tj|tj|d{VdS)zg Clear the queue when the restore fails, set hits' status back to cleanup_done N)r(db_hits_under_restorationrr$r$rrs rCrz+RestoreOriginalDb.update_failed_hits_statusQsW 355d$4$ABBB  """""""""""rEN)rrrrrer;rUrrr^rr(rrrMalwareDatabaseRestoreTaskrMalwareRestoreCompleterr-rrMalwareDatabaseRestoreFailedrrFrErCrrs\ KE  \ .### z" *    \  VK 233##43# VK .//660/6( VK .//##0/# VK .//  0/ D VK 455##65###rEr)grUrbrrrr contextlibr functoolsrloggingrpathlibrtypingrrr r defence360agentr defence360agent.apir defence360agent.contracts.configr rr%defence360agent.contracts.hook_eventsr!defence360agent.contracts.licenser"defence360agent.contracts.messagesr%defence360agent.contracts.permissionsr!defence360agent.contracts.pluginsrrr&defence360agent.internals.global_scoperdefence360agent.utilsrrrr'defence360agent.subsys.persistent_stater defence360agent.utils.check_lockrdefence360agent.utils.commonrrrimav.malwarelib.cleanup.cleanerr r!r"imav.malwarelib.cleanup.storager#imav.malwarelib.configr$r%r&imav.malwarelib.modelr'r(imav.malwarelib.scanr) imav.malwarelib.scan.mds.cleanerr*!imav.malwarelib.scan.mds.detachedr+r, imav.malwarelib.scan.mds.restorer-imav.malwarelib.subsys.malwarer.r/imav.malwarelib.utilsr0imav.malwarelib.utils.user_listr1r2rr{rAV_IM360rXgroup_by_attributerrrrrrHrJr r=rr]rhrrFrErCrsB* ............!!!!!!******<;;;;;888888::::::NNNNNN 544444 GFFFFF777777@@@@@@@@@@ ;::::: =<<<<<<<999999CCCCCCDCCCCCMMMMMMMM222222 8  '($  y%. 9 9 7:8HMMM6'JJJDjjV^DDD L I8J/IHIIIIVVVVVk=VVVr fffffk=fffR))))) )))X   @@@@@?@@@$g g g g g g g g T~#~#~#~#~# ~#~#~#~#~#rE